<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Redis面经 | 无人深空</title><meta name="keywords" content="Redis,DataBase"><meta name="author" content="zcm"><meta name="copyright" content="zcm"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#ffffff"><meta name="description" content="Redis 面经 Redis 是一个使用 C 语言写成的，开源的高性能 key-value 非关系缓存数据库 支持存储的 value 类型相对更多，包括 string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)、hash（哈希类型） Redis 的数据都基于缓存的，每秒可以处理超过 10 万次读写操作，是已知性能最快的 Key-Value DB Re">
<meta property="og:type" content="article">
<meta property="og:title" content="Redis面经">
<meta property="og:url" content="https://gitee.com/zcmmmm/zcmmmm/2022/10/14/Redis%E9%9D%A2%E7%BB%8F/index.html">
<meta property="og:site_name" content="无人深空">
<meta property="og:description" content="Redis 面经 Redis 是一个使用 C 语言写成的，开源的高性能 key-value 非关系缓存数据库 支持存储的 value 类型相对更多，包括 string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)、hash（哈希类型） Redis 的数据都基于缓存的，每秒可以处理超过 10 万次读写操作，是已知性能最快的 Key-Value DB Re">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis.png">
<meta property="article:published_time" content="2022-10-14T10:43:08.000Z">
<meta property="article:modified_time" content="2023-05-18T11:32:09.803Z">
<meta property="article:author" content="zcm">
<meta property="article:tag" content="Redis">
<meta property="article:tag" content="DataBase">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis.png"><link rel="shortcut icon" href="/img/favicon.png"><link rel="canonical" href="https://gitee.com/zcmmmm/zcmmmm/2022/10/14/Redis%E9%9D%A2%E7%BB%8F/"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.min.css" media="print" onload="this.media='all'"><script>const GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: {"path":"/search.xml","preload":false,"languages":{"hits_empty":"找不到您查询的内容：${query}"}},
  translate: undefined,
  noticeOutdate: undefined,
  highlight: {"plugin":"highlighjs","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":200},
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '天',
  date_suffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: undefined,
  lightbox: 'fancybox',
  Snackbar: {"chs_to_cht":"你已切换为繁体","cht_to_chs":"你已切换为简体","day_to_night":"你已切换为深色模式","night_to_day":"你已切换为浅色模式","bgLight":"#49b1f5","bgDark":"#1f1f1f","position":"bottom-left"},
  source: {
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.css'
    }
  },
  isPhotoFigcaption: false,
  islazyload: false,
  isAnchor: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'Redis面经',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2023-05-18 19:32:09'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          if (t === 'dark') activateDarkMode()
          else if (t === 'light') activateLightMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script><link rel="stylesheet" href="/css/categories.css?v1"><link rel="stylesheet" href="/css/navigation.css?v1"><link rel="stylesheet" href="/css/custom.css?v1"><meta name="generator" content="Hexo 6.3.0"></head><body><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="https://s2.loli.net/2022/09/22/36LN4kcjYOd9HT8.jpg" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">43</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">25</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">11</div></a></div><hr/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="not-top-img" id="page-header"><nav id="nav"><span id="blog_name"><a id="site-name" href="/">无人深空</a></span><div id="menus"></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div></div><div id="nav-right"><div id="search-button"><a class="site-page social-icon search"><i class="fas fa-search fa-fw"></i></a></div><div id="toggle-menu"><a class="site-page"><i class="fas fa-bars fa-fw"></i></a></div></div></nav></header><main class="layout" id="content-inner"><div id="post"><div id="post-info"><h1 class="post-title">Redis面经</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2022-10-14T10:43:08.000Z" title="发表于 2022-10-14 18:43:08">2022-10-14</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2023-05-18T11:32:09.803Z" title="更新于 2023-05-18 19:32:09">2023-05-18</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/%E9%9D%A2%E7%BB%8F/">面经</a><i class="fas fa-angle-right post-meta-separator"></i><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/DataBase/">DataBase</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">19k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>60分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="Redis面经"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div><article class="post-content" id="article-container"><h2 id="Redis-面经"><a href="#Redis-面经" class="headerlink" title="Redis 面经"></a>Redis 面经</h2><blockquote>
<p>Redis 是一个使用 C 语言写成的，开源的高性能 key-value 非关系缓存数据库</p>
<p>支持存储的 value 类型相对更多，包括 <code>string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)、hash（哈希类型）</code></p>
<p>Redis 的数据都基于缓存的，每秒可以处理超过 10 万次读写操作，是已知性能最快的 Key-Value DB</p>
<p>Redis 也可以实现数据写入磁盘中，保证了数据的安全不丢失</p>
<p><strong>Redis 的操作是原子性的，因为 Redis 是单线程的</strong></p>
<p><strong>Redis 本身提供的所有 API 都是原子操作，Redis 中的事务其实是要保证批量操作的有序性，不受其他客户端命令干扰（不能保证原子性）</strong></p>
</blockquote>
<h3 id="1-Redis-单线程原理"><a href="#1-Redis-单线程原理" class="headerlink" title="1.Redis 单线程原理"></a>1.Redis 单线程原理</h3><blockquote>
<p>首先必须明确，Redis 单线程指的是网络请求模块使用了一个线程（其他模块仍用了多个线程）</p>
<p>并不是一个线程完成了所有功能</p>
<p>原理上，其采用了利用 epoll 的多路复用特性，因此可以采用单线程处理其网络请求</p>
<p>Redis 是单进程单线程的，redis 利用队列技术将并发访问变为串行访问， 消除了传统数据库串行控制的开销</p>
</blockquote>
<ul>
<li><p>Redis 是单线程还是多线程</p>
<blockquote>
<p>这个问题不能直接答是单线程还是多线程，可以分redis的版本去回答</p>
<ul>
<li><p>redis-2.6 开始支持lua脚本</p>
</li>
<li><p>3.X redis支持集群 是单线程</p>
</li>
<li><p>4.X 严格来说不是单线程，负责处理客户端的线程是单线程，删除使用多线程异步删除，从而引入了多线程</p>
</li>
<li><p>5.X  redis代码重构</p>
</li>
<li><p>6.X 告别大家印象中的单线程，用一种全新的多线程解决问题</p>
</li>
</ul>
</blockquote>
</li>
<li><p><strong>Redis3.x 单线程时代但性能依旧很快的主要原因</strong></p>
<ul>
<li>基于内存操作：Redis 的所有数据都存在内存中，因此所有的运算都是内存级别的，所以他的性能比较高</li>
<li>数据结构简单：Redis 的数据结构是专门设计的，而这些简单的数据结构的查找和操作的时间大部分复杂度都是 O(1)，因此性能比较高</li>
<li>多路复用和非阻塞 I&#x2F;O：Redis 使用 I&#x2F;O 多路复用功能来监听多个 socket 连接客户端，这样就可以使用一个线程连接来处理多个请求，减少线程切换带来的开销，同时也避免了 I&#x2F;O 阻塞操作</li>
<li>避免上下文切换：因为是单线程模型，因此就避免了不必要的上下文切换和多线程竞争，这就省去了多线程切换带来的时间和性能上的消耗，而且单线程不会导致死锁问题的发生</li>
<li>作者原话大致解析：Redis 是基于内存操作的，因此他的瓶颈可能是机器的内存或者网络带宽而并非 CPU，既然 CPU 不是瓶颈，那么自然就采用单线程的解决方案了，况且使用多线程比较麻烦</li>
</ul>
</li>
<li><p>为什么逐渐加入了多线程特性？</p>
<blockquote>
<p>正常情况下使用 del 指令可以很快的删除数据，而当被删除的 key 是一个非常大的对象时，例如包含了成千上万个元素的 hash 集合时，那么 del 指令就会造成 Redis 主线程卡顿</p>
<p> 这就是 <code>redis3.x</code> 单线程时代最经典的故障，大 key 删除的头疼问题， 由于redis是单线程的，del bigKey ….. 等待很久这个线程才会释放，类似加了一个 synchronized 锁，你可以想象高并发下，程序堵成什么样子？</p>
</blockquote>
</li>
<li><p>Redis6.0 默认是否开启了多线程？</p>
<blockquote>
<p>Redis 将所有数据放在内存中，内存的响应时长大约为 100 纳秒</p>
<p>对于小数据包，Redis 服务器可以处理 8W 到 10W 的 QPS（每秒查询率），这也是 Redis 处理的极限了，对于 80% 的公司来说，单线程的 Redis 已经足够使用了</p>
<p>在 Redis6.0 中，多线程机制默认是关闭的，如果需要使用多线程功能，需要在 <code>redis.conf</code> 中完成两个设置</p>
<ul>
<li>设置 <code>io-thread-do-reads</code> 配置项为 yes，表示启动多线程</li>
<li>设置线程个数。关于线程数的设置，官方的建议是如果为 4 核的 CPU，建议线程数设置为 2 或 3，如果为 8 核 CPU 建议线程数设置为 6，线程数一定要小于机器核数，线程数并不是越大越好</li>
</ul>
</blockquote>
</li>
</ul>
<h3 id="2-Redis-数据类型（5-3）"><a href="#2-Redis-数据类型（5-3）" class="headerlink" title="2.Redis 数据类型（5 + 3）"></a>2.Redis 数据类型（5 + 3）</h3><ul>
<li><p>String：字符串类型，最简单的类型 </p>
<blockquote>
<p><strong>一个 Redis 中字符串 value 最多可以是 512M</strong></p>
<p>为什么 Redis 不采用 C 语言的字符串实现？</p>
<ul>
<li>获取长度的时间复杂度为 O(N)：<code>一直读取到 \0 为之</code></li>
<li>没有较好的扩容机制</li>
<li>特殊字符无法处理</li>
</ul>
<p>Redis 字符串本质是 C 的字符数组 + 一些其他标识属性，有如下优点</p>
<ul>
<li>获取长度时间复杂度为 O(1)</li>
<li>减少了字符串扩容引起的数据搬运次数</li>
<li>可以存储更加复杂的二进制数据</li>
</ul>
</blockquote>
<blockquote>
<p><strong>应用场景：文章浏览量，点击时使用 <code>incr key</code> 命令增加一个数字 1，完成记录</strong></p>
</blockquote>
</li>
<li><p>Hash：类似于 Map 的一种结构</p>
<blockquote>
<p>采用拉链法解决 hash 冲突</p>
<p>Hash 类型对应的数据结构是两种：ziplist（压缩列表），hashtable（哈希表）</p>
<p>当 field-value 长度较短且个数较少时，使用 ziplist，否则使用 hashtable</p>
<p>三个主要数据结构</p>
<ul>
<li><p>dictht：表示一个 hash 表</p>
 <figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictht</span> &#123;</span></span><br><span class="line">    dictEntry **table; <span class="comment">/* 链表数组 */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> size; <span class="comment">/* bucket 的数量 */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> sizemask;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> used; <span class="comment">/* 存储元素的数量 */</span></span><br><span class="line">&#125; dictht;</span><br></pre></td></tr></table></figure>
</li>
<li><p>dictEntry：哈希表的一项，可看作一个键值对</p>
  <figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> &#123;</span></span><br><span class="line">    <span class="type">void</span> *key;</span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        <span class="type">void</span> *val;</span><br><span class="line">        <span class="type">uint64_t</span> u64;</span><br><span class="line">        <span class="type">int64_t</span> s64;</span><br><span class="line">        <span class="type">double</span> d;</span><br><span class="line">    &#125; v;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> *<span class="title">next</span>;</span></span><br><span class="line">&#125; dictEntry;</span><br></pre></td></tr></table></figure>
</li>
<li><p>dict：redis 给外层调用的哈系表结构，包含两个 dictht</p>
<blockquote>
<p>为什么有两个 dictht：<a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/358366217">渐进 rehash</a></p>
</blockquote>
 <figure class="highlight c"><table><tr><td class="code"><pre><span class="line"> <span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dict</span> &#123;</span></span><br><span class="line">     dictType *type;</span><br><span class="line">     <span class="type">void</span> *privdata;</span><br><span class="line">     dictht ht[<span class="number">2</span>];</span><br><span class="line">     <span class="type">long</span> rehashidx; <span class="comment">/* rehashing not in progress if rehashidx == -1 */</span></span><br><span class="line">     <span class="type">int16_t</span> pauserehash; <span class="comment">/* If &gt;0 rehashing is paused (&lt;0 indicates coding error) */</span></span><br><span class="line">&#125; dict;</span><br></pre></td></tr></table></figure>

<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/dict%E5%8E%9F%E7%90%86.png" alt="dict原理"></p>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/hash%E5%BA%95%E5%B1%82.png" alt="hash底层"></p>
</blockquote>
<blockquote>
<p><strong>应用场景：购物车添加商品</strong></p>
<p>新增商品 <code>hset shopcar:uid1024 334488 1</code></p>
<p>新增商品 <code>hset shopcar:uid1024 334477 1</code></p>
<p>增加商品数量 <code>hincrby shopcar:uid1024 334477 1</code></p>
<p>商品总数 <code>hlen shopcar:uid1024</code></p>
<p>全部选择 <code>hgetall shopcar:uid102</code></p>
</blockquote>
</li>
<li><p>List：有序列表</p>
<blockquote>
<p>应用场景：微信公众号订阅的消息</p>
<p>底层实现是双向链表</p>
<p>List 的数据结构为快速链表 <code>quickList</code></p>
<p>首先在列表元素较少的情况下会使用一块连续的内存存储，这个结构是 <code>ziplist</code>，即压缩列表，它将所有的元素紧挨着一起存储，分配的是一块连续的内存</p>
<p>当数据量比较多的时候才会改成 <code>quicklist</code></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/List%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png" alt="List数据结构"></p>
<p><strong>Redis 将链表和 ziplist 结合起来组成了 quicklist</strong></p>
<p><strong>也就是将多个 ziplist 使用双向指针串起来使用</strong></p>
<p><strong>这样既满足了快速的插入删除性能，又不会出现太大的空间冗余</strong></p>
</blockquote>
<blockquote>
<p>应用场景：朋友圈点赞</p>
<p>新增点赞 <code>sadd pub:msgID 点赞用户ID1 点赞用户ID2</code></p>
<p>取消点赞 <code>srem pub:msgID 点赞用户ID </code></p>
<p>展现所有点赞过的用户 <code>SMEMBERS pub:msgID </code></p>
<p>点赞用户数统计，就是常见的点赞红色数字 <code>scard pub:msgID </code></p>
<p>判断某个朋友是否对楼主点赞过 <code>SISMEMBER pub:msgID 用户ID</code></p>
</blockquote>
</li>
<li><p>Set：无序集合</p>
<blockquote>
<p>Redis set 对外提供的功能与 list 类似是一个列表的功能，特殊之处在于 set 是可以<strong>自动排重</strong></p>
<p>Redis 的 Set 是 string 类型的无序集合</p>
<p>它底层其实是一个 value 为 null 的 hash 表，所以添加、删除、查找的<strong>复杂度都是O(1)</strong></p>
<p>Set 数据结构是 dict 字典，字典是用哈希表实现的</p>
<p>Java 中 HashSet 的内部实现使用的是 HashMap，只不过所有的 value 都指向同一个对象</p>
<p>Redis 的 set 结构也是一样，它的内部也使用 hash 结构，所有的 value 都指向同一个内部值</p>
</blockquote>
<blockquote>
<p>应用场景：抽奖小程序</p>
<p>立即参与按钮 <code>sadd key 用户ID</code></p>
<p>抽奖（从set中任意选取N个中奖人） <code>SCARD key </code></p>
<p>随机抽奖2个人，元素不删除 <code>SRANDMEMBER key 2 </code></p>
<p>随机抽奖3个人，元素会删除 <code>SPOP key 3</code></p>
</blockquote>
</li>
<li><p>ZSet：带权值的无序集合，即每个 ZSet 元素还另有一个数字代表权值，集合通过权值进行排序</p>
<blockquote>
<p>zset 底层使用了两个数据结构：</p>
<ul>
<li><p>hash：hash 的作用就是关联元素 value 和权重 score，保障元素 value 的唯一性，可以通过元素 value 找到相应的 score 值</p>
</li>
<li><p>跳跃表：跳跃表的目的在于给元素 value 排序，根据 score 的范围获取元素列表</p>
</li>
</ul>
</blockquote>
<blockquote>
<p>应用场景：根据商品销售对商品进行排序显示</p>
<p>商品销售排行榜（sorted set集合），key 为 <code>goods:sellsort</code>，分数为商品销售数量</p>
</blockquote>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%BA%95%E5%B1%82%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%AF%B9%E5%BA%94%E5%85%B3%E7%B3%BB.png" alt="Redis数据类型和底层数据结构对应关系"></p>
<ul>
<li><p>bitmaps</p>
<blockquote>
<p>应用场景：京东签到领取京豆</p>
<p>签到日历仅展示当月签到数据<br>签到日历需展示最近连续签到天数<br>假设当前日期是 20210618，且 20210616 未签到<br>若 20210617 已签到且 0618 未签到，则连续签到天数为 1<br>若 20210617 已签到且 0618 已签到，则连续签到天数为 2</p>
</blockquote>
</li>
<li><p>hyperloglog</p>
<blockquote>
<p>应用场景：天猫网站首页亿级 UV 的 Redis 统计方案-统计日活</p>
</blockquote>
</li>
<li><p>geospatial</p>
<blockquote>
<p>应用场景：美团地图位置附近的酒店推送</p>
</blockquote>
</li>
</ul>
<h3 id="3-什么情况下用-Redis"><a href="#3-什么情况下用-Redis" class="headerlink" title="3.什么情况下用 Redis"></a>3.什么情况下用 Redis</h3><ul>
<li><p><strong>针对热点数据进行缓存</strong></p>
</li>
<li><p>对于特定限时数据的存放</p>
</li>
<li><p>针对带热点权值数据的排序 list</p>
</li>
<li><p><strong>分布式锁</strong></p>
<blockquote>
<p>Redis 做分布式锁的时候有需要注意的问题</p>
</blockquote>
<ul>
<li><p>独占性</p>
<blockquote>
<p>OnlyOne，任何时刻只能有且仅有一个线程持有</p>
</blockquote>
</li>
<li><p>高可用</p>
<blockquote>
<p>若 redis 集群环境下，不能因为某一个节点挂了而出现获取锁和释放锁失败的情况</p>
</blockquote>
</li>
<li><p>防死锁</p>
<blockquote>
<p>杜绝死锁，必须有超时控制机制或者撤销操作，有个兜底终止跳出方案</p>
</blockquote>
</li>
<li><p>不乱抢</p>
<blockquote>
<p>防止张冠李戴，不能私下unlock别人的锁，只能自己加锁自己释放</p>
</blockquote>
</li>
<li><p>重入性</p>
<blockquote>
<p>同一个节点的同一个线程如果获得锁之后，它也可以再次获取这个锁</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="4-Redis-与-memcache-的区别"><a href="#4-Redis-与-memcache-的区别" class="headerlink" title="4.Redis 与 memcache 的区别"></a>4.Redis 与 memcache 的区别</h3><ul>
<li>redis 处理网络请求采用单线程模型（6.x 之前单线程，6.x 及之后多线程），而 memcache 采用多线程异步 IO 的方式</li>
<li>redis 支持数据持久化，memcache 不支持</li>
<li>redis 支持的数据格式比 memcache 更多</li>
</ul>
<hr>
<h3 id="5-简述缓存穿透"><a href="#5-简述缓存穿透" class="headerlink" title="5.简述缓存穿透"></a>5.简述缓存穿透</h3><blockquote>
<p><strong>缓存穿透指缓存和数据库均没有需要查询的数据</strong>，攻击者不断发送这种请求，使数据库压力过大</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F.png" alt="缓存穿透"></p>
<h3 id="6-简述缓存穿透的解决方法"><a href="#6-简述缓存穿透的解决方法" class="headerlink" title="6.简述缓存穿透的解决方法"></a>6.简述缓存穿透的解决方法</h3><ul>
<li><p><strong>设置可访问的名单（白名单）</strong></p>
<blockquote>
<p>使用 bitmaps 类型定义一个可以访问的名单，名单 id 作为 bitmaps 的偏移量，每次访问和 bitmap 里面的 id 进行比较，如果访问 id 不在 bitmaps 里面，则进行拦截，不允许访问</p>
</blockquote>
</li>
<li><p><strong>对空值缓存</strong></p>
<blockquote>
<p>如果一个查询返回的数据为空（不管是数据是否不存在），我们仍然把这个空结果（null）进行缓存，设置空结果的过期时间会很短，最长不超过五分钟</p>
</blockquote>
</li>
<li><p><strong>布隆过滤器</strong></p>
<blockquote>
<p>它实际上是一个很长的二进制向量（位图）和一系列随机映射函数（哈希函数）</p>
<p>布隆过滤器可以用于检索一个元素是否在一个集合中</p>
<p><strong>它的优点是空间效率和查询时间都远远超过一般的算法，缺点是有一定的误识别率和删除困难</strong></p>
<p>将所有可能存在的数据哈希到一个足够大的 bitmaps 中，一个一定不存在的数据会被 这个 bitmaps 拦截掉，从而避免了对底层存储系统的查询压力</p>
</blockquote>
</li>
<li><p><strong>进行实时监控</strong></p>
<blockquote>
<p>当发现 Redis 的命中率开始急速降低，需要排查访问对象和访问的数据，和运维人员配合，可以设置黑名单限制服务</p>
</blockquote>
</li>
</ul>
<h3 id="7-简述缓存击穿"><a href="#7-简述缓存击穿" class="headerlink" title="7.简述缓存击穿"></a>7.简述缓存击穿</h3><blockquote>
<p><strong>热点数据缓存过期</strong></p>
<p><strong>缓存击穿指缓存中没有数据，但数据库中有该数据</strong></p>
<p>一般这种情况指特定数据的缓存时间到期，但由于并发用户访问该数据特别多，因此去数据库去取数据，引起数据库访问压力过大</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.png" alt="缓存击穿"></p>
<h3 id="8-简述缓存击穿的解决方法"><a href="#8-简述缓存击穿的解决方法" class="headerlink" title="8.简述缓存击穿的解决方法"></a>8.简述缓存击穿的解决方法</h3><ul>
<li><p><strong>预先设置热门数据</strong></p>
<blockquote>
<p>在 redis 高峰访问之前，把一些热门数据提前存入到 redis 里面，加大这些热门数据 key 的时长</p>
</blockquote>
</li>
<li><p><strong>逻辑过期</strong></p>
<blockquote>
<p>加一个字段设置逻辑过期时间，用于数据更新维护</p>
<p>数据不会真正过期，永远都能查到</p>
</blockquote>
</li>
<li><p><strong>对并发读数据设置并发锁，降低并发性</strong></p>
<blockquote>
<p>就是在缓存失效的时候（判断拿出来的值为空），不是立即去 <code>load db</code></p>
<p>先使用缓存工具的某些带成功操作返回值的操作（比如 Redis 的 SETNX）去 set 一个 mutex key</p>
<p>当操作返回成功时，再进行 load db 的操作，并回设缓存，最后删除 mutex key</p>
<p>当操作返回失败，证明有线程在 load db，当前线程睡眠一段时间再重试整个 get 缓存的方法</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E9%94%81.png" alt="锁"></p>
</blockquote>
</li>
<li><p><strong>实时调整</strong></p>
<blockquote>
<p>现场监控哪些数据热门，实时调整 key 的过期时长</p>
</blockquote>
</li>
</ul>
<h3 id="9-简述缓存雪崩"><a href="#9-简述缓存雪崩" class="headerlink" title="9.简述缓存雪崩"></a>9.简述缓存雪崩</h3><blockquote>
<p><strong>缓存雪崩指缓存中一大批数据到过期时间，而从缓存中删除</strong></p>
<p>但该批数据查询数据量巨大，查询全部走数据库，造成数据库压力过大</p>
<p><strong>缓存雪崩与缓存击穿的区别：</strong></p>
<ul>
<li><p><strong>缓存雪崩针对很多 key 缓存</strong></p>
</li>
<li><p><strong>缓存击穿针对某一个 key 正常访问</strong></p>
</li>
</ul>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9.png" alt="缓存雪崩"></p>
<h3 id="10-简述缓存雪崩的解决方法"><a href="#10-简述缓存雪崩的解决方法" class="headerlink" title="10.简述缓存雪崩的解决方法"></a>10.简述缓存雪崩的解决方法</h3><ul>
<li><p><strong>将缓存失效时间分散开</strong></p>
<blockquote>
<p>缓存数据设置随机过期时间，防止同一时间大量数据过期</p>
<p>对于集群部署的情况，将热点数据均与分布在不同缓存中</p>
</blockquote>
</li>
<li><p><strong>设置热点数据永远不过期</strong></p>
</li>
<li><p><strong>使用锁或队列</strong></p>
<blockquote>
<p>用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写，从而避免失效时大量的并发请求落到底层存储系统上</p>
<p>不适用高并发情况</p>
</blockquote>
</li>
<li><p><strong>设置过期标志更新缓存</strong></p>
<blockquote>
<p>记录缓存数据是否过期（设置提前量），如果过期会触发通知另外的线程在后台去更新实际 key 的缓存</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="11-Redis-有哪些集群部署方式"><a href="#11-Redis-有哪些集群部署方式" class="headerlink" title="11.Redis 有哪些集群部署方式"></a>11.Redis 有哪些集群部署方式</h3><ul>
<li>主从复制</li>
<li>哨兵模式</li>
<li>Cluster 集群模式</li>
</ul>
<h3 id="12-简述主从复制模式"><a href="#12-简述主从复制模式" class="headerlink" title="12.简述主从复制模式"></a>12.简述主从复制模式</h3><blockquote>
<p>主机数据更新后根据配置和策略， 自动同步到备机的 <code>master/slaver</code> 机制</p>
<p><strong>Master 以写为主，Slave 以读为主</strong></p>
</blockquote>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E4%BD%9C%E7%94%A8.jpg" alt="主从复制作用"></p>
<blockquote>
<p>在主从复制中，有主库（Master）节点和从库（Slave）节点两个角色</p>
<p>从节点服务启动会连接主库，并向主库发送 PSYNC 命令</p>
<p>主节点收到同步命令，启动持久化工作，工作执行完成后，主节点将传送整个数据库文件到从库，从节点接收到数据库文件数据之后将数据进行加载</p>
<p>此后，主节点继续将所有已经收集到的修改命令，和新的修改命令依次传送给从节点，从节点依次执行，从而达到最终的数据同步</p>
<p><strong>通过这种方式，可以使写操作作用于主库，而读操作作用于从库，从而达到读写分离</strong></p>
</blockquote>
<ul>
<li><p>主从复制的作用</p>
<ul>
<li>读写分离，性能扩展</li>
<li>容灾快速恢复</li>
</ul>
</li>
<li><p>常用主从复制</p>
<ul>
<li>一主二仆</li>
<li>薪火相传</li>
<li>反客为主（哨兵模式手动版）</li>
</ul>
</li>
<li><p>复制原理（同步机制）</p>
<ul>
<li>Slave 启动成功连接到 master 后会发送一个 sync 命令</li>
<li>Master 接到命令启动后台的存盘进程，同时收集所有接收到的用于修改数据集命令， 在后台进程执行完毕之后，master 将传送整个数据文件到 slave，以完成一次完全同步</li>
<li>全量复制：而 slave 服务在接收到数据库文件数据后，将其存盘并加载到内存中</li>
<li>增量复制：Master 继续将新的所有收集到的修改命令依次传给 slave，完成同步</li>
<li>但是只要是重新连接 master，一次完全同步（全量复制）将被自动执行</li>
</ul>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86.jpg" alt="复制原理"></p>
</li>
<li><p>主从复制问题</p>
<blockquote>
<p>主机 shutdown 后情况如何？从机是上位还是原地待命？</p>
<p>Answer：从机知道主机 shutdown，但是还是保持从机身份</p>
<hr>
<p>主机又回来了后，主机新增记录，从机还能否顺利复制？ </p>
<p>Answer：可以</p>
<hr>
<p>其中一台从机 shutdown 后情况如何？重新开机后它能跟上大部队吗？</p>
<p>Answer：可以</p>
</blockquote>
</li>
</ul>
<h3 id="13-简述哨兵模式"><a href="#13-简述哨兵模式" class="headerlink" title="13.简述哨兵模式"></a>13.简述哨兵模式</h3><blockquote>
<p><strong>哨兵模式监控 redis 集群中 Master 的工作的状态</strong></p>
<p><strong>在 Master 主服务器宕机时，从 slave 中选择新机器当作 master，保证系统高可用</strong></p>
<p>每个哨兵每 10 秒向主服务器，slave 和其他哨兵发送 ping</p>
<p>客户端通过哨兵，由哨兵提供可供服务的 redis master 节点</p>
<p>哨兵只需要配 master 节点，会自动寻找其对应的 slave 节点</p>
<p>监控同一 master 节点的哨兵会自动互联，组成哨兵网络，当任一哨兵发现 master 连接不上，即开会投票，投票半数以上决定 Master 下线，并从 slave 节点中选取下一任 master 节点</p>
</blockquote>
<h3 id="14-cluster-集群"><a href="#14-cluster-集群" class="headerlink" title="14.cluster 集群"></a>14.cluster 集群</h3><blockquote>
<p>cluster提出了虚拟槽的概念</p>
<ol>
<li><code>redis cluster</code> 默认有 16384 个槽，在集群搭建的时候，需要给节点分配哈希槽尽可能相同数量虚拟槽</li>
<li>如果目前 redis 执行 set 操作，redis 先对这个 key 经过 CRC16 hash 运算，并把结果对 16384 取余，得到槽编号</li>
<li>根据槽编号，寻找到其对应的 redis 节点，在节点上执行 hash 命令</li>
<li>如果此时执行 get 操作，节点先验证该 key 对应的槽编号是不是归本节点管，如果是则保存数据。如果不是则发送正确节点编号给客户端</li>
</ol>
</blockquote>
<blockquote>
<p>举个例子， 如果一个集群可以有主节点， 其中：</p>
<p>节点 A 负责处理 0 号至 5460 号插槽</p>
<p>节点 B 负责处理 5461 号至 10922 号插槽</p>
<p>节点 C 负责处理 10923 号至 16383 号插槽</p>
</blockquote>
<ul>
<li><p>故障恢复</p>
<blockquote>
<p>如果主节点下线？从节点能否自动升为主节点？注意：<strong>15 秒超时</strong></p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E6%95%85%E9%9A%9C%E6%81%A2%E5%A4%8D1.jpg" alt="故障恢复1"></p>
<p>主节点恢复后，主从关系会如何？主节点回来变成从机</p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E6%95%85%E9%9A%9C%E6%81%A2%E5%A4%8D2.jpg" alt="故障恢复2"></p>
<p>如果所有某一段插槽的主从节点都宕掉，redis 服务是否还能继续?</p>
<p>如果某一段插槽的主从都挂掉，而 <code>cluster-require-full-coverage=yes</code> ，那么整个集群都挂掉</p>
<p>如果某一段插槽的主从都挂掉，而 <code>cluster-require-full-coverage=no</code> ，那么该插槽数据全都不能使用，也无法存储（当 key 值位于故障节点的插槽时，无法进行正常操作）</p>
<p>redis.conf 中的参数 <code>cluster-require-full-coverage</code></p>
</blockquote>
</li>
<li><p>为什么最大槽数为 16384 个？</p>
<blockquote>
<p>CRC16 算法产生的 hash 值有 16bit，该算法可以产生 2^16&#x3D;65536 个值</p>
<p>换句话说值是分布在 0~65535 之间。那作者在做 mod 运算的时候，为什么不 mod65536，而选择 mod16384？</p>
</blockquote>
<ul>
<li><p>如果槽位为 65536，发送心跳信息的消息头达 8k，发送的心跳包过于庞大</p>
<blockquote>
<p>在消息头中最占空间的是 <code>myslots[CLUSTER_SLOTS/8]</code></p>
<p> 当槽位为 65536 时，这块的大小是：<code>65536/8/1024=8kb</code>（16384 &#x2F; 8 &#x2F; 1024 &#x3D; 2kb）</p>
<p>因为每秒钟，redis 节点需要发送一定数量的 ping 消息作为心跳包，如果槽位为 65536，这个 ping 消息的消息头太大了，浪费带宽</p>
</blockquote>
</li>
<li><p>redis的集群主节点数量基本不可能超过 1000 个</p>
<blockquote>
<p>集群节点越多，心跳包的消息体内携带的数据越多。如果节点过1000个，也会导致网络拥堵</p>
<p>因此 redis 作者不建议 redis cluster 节点数量超过 1000 个</p>
<p>对于节点数 &lt;1000 的 redis cluster 集群，16384 个槽位够用了，没必要拓展到 65536 个</p>
</blockquote>
</li>
<li><p>槽位越小，节点少的情况下，压缩比高，容易传输</p>
<blockquote>
<p>Redis 主节点的配置信息中它所负责的哈希槽是通过一张 bitmap 的形式来保存的，在传输过程中会对 bitmap 进行压缩</p>
<p>但是如果 bitmap 的填充率 slots &#x2F; N 很高的话（N表示节点数），bitmap的压缩率就很低（节点数很少，而哈希槽数量很多的话，bitmap 的压缩率就很低）</p>
</blockquote>
</li>
</ul>
</li>
<li><p><a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/129049724">一致性哈希算法（consistent hashing）</a></p>
<p><a target="_blank" rel="noopener" href="https://juejin.cn/post/7064557796762583047">Redis Cluster 为什么选哈希槽不选一致性哈希</a></p>
</li>
<li><p>集群的<strong>单点登录</strong>问题</p>
<blockquote>
<p>单点登录 Single Sign On（简称<strong>SSO</strong>）</p>
<p>在多个应用系统中，用户只需要登录一次就可以访问所有相互信任的应用系统</p>
<p>单系统登录功能主要是用 Session 保存用户信息来实现的，但是集群有多个 redis 服务器，redisA 节点的 Session 和 redisB 节点的 Session 是<strong>不共享</strong>的</p>
<p>用户第一次登录转发到了 redisA 节点上，并创建了相应的 session，但是下次登录转发到 redisB 节点上时无法识别到该用户已登录</p>
</blockquote>
<ul>
<li><p>粘性 session</p>
<blockquote>
<p>指 Nginx 每次都将同一用户的请求转发至同一节点，即将用户与节点绑定</p>
</blockquote>
</li>
<li><p>节点 session 复制</p>
<blockquote>
<p>每次 session 发生变化时（创建、修改）广播给集群中所有节点，使得所有节点的 session 相同</p>
</blockquote>
</li>
<li><p>session 共享</p>
<blockquote>
<p>缓存 session</p>
</blockquote>
</li>
<li><p>session 持久化</p>
<blockquote>
<p>将 session 存储至数据库中，向操作数据一样操作 session</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="15-简述-Redis-的-RDB"><a href="#15-简述-Redis-的-RDB" class="headerlink" title="15.简述 Redis 的 RDB"></a>15.简述 Redis 的 RDB</h3><blockquote>
<p>Redis Data Base</p>
<p>RDB 即将当前数据生成快照，并保存于硬盘中</p>
<p>在指定的时间间隔内将内存中的数据集快照写入磁盘， 即 Snapshot 快照，它恢复时是将快照文件直接读到内存里</p>
<p>可以通过手动命令，也可以设置自动触发</p>
<p><strong>如果需要进行大规模数据的恢复，且对于数据恢复的完整性不是非常敏感，那 RDB 方式要比 AOF 方式更加的高效</strong></p>
<p><strong>RDB的缺点是最后一次持久化后的数据可能丢失</strong></p>
<p><strong>全量备份</strong></p>
</blockquote>
<h3 id="16-简述-Redis-的-save-命令"><a href="#16-简述-Redis-的-save-命令" class="headerlink" title="16.简述 Redis 的 save 命令"></a>16.简述 <code>Redis</code> 的 save 命令</h3><blockquote>
<p><strong>save 命令是 <code>redis</code> 手动触发 RDB 过程的命令</strong></p>
<p><strong>使用该命令后，服务器阻塞，直到 RDB 过程完成后终止</strong></p>
<p><strong>该过程占用内存较多</strong></p>
</blockquote>
<h3 id="17-简述-Redis-的-bgsave-命令"><a href="#17-简述-Redis-的-bgsave-命令" class="headerlink" title="17.简述 Redis 的 bgsave 命令"></a>17.简述 <code>Redis</code> 的 <code>bgsave</code> 命令</h3><blockquote>
<p><code>bgsave</code> 命令不阻塞主进程（严格意义上也不是完全不阻塞，详看下面过程）</p>
<p>该命令 fork 一个子进程用于执行 RDB 过程</p>
<p>其具体过程为：</p>
<ol>
<li>判断此时有没有子进程用于 RDB，有的话直接返回</li>
<li>redis 进行 fork 子进程过程，此时父进程处于非阻塞状态</li>
<li>子进程创建 RDB 文件，完成后返回给父进程</li>
</ol>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/RDB%E6%8C%81%E4%B9%85%E5%8C%96%E6%B5%81%E7%A8%8B.png" alt="RDB持久化流程"></p>
<h3 id="18-简述-Redis-自动触发-RDB-机制"><a href="#18-简述-Redis-自动触发-RDB-机制" class="headerlink" title="18.简述 Redis 自动触发 RDB 机制"></a>18.简述 Redis 自动触发 RDB 机制</h3><ul>
<li>通过配置文件，设置一定时间后自动执行 RDB</li>
<li>如采用主从复制过程，会自动执行 RDB</li>
<li>Redis 执行 shutdown 时，在未开启 AOF 后会执行 RDB</li>
</ul>
<h3 id="19-简述-Redis-的-AOF"><a href="#19-简述-Redis-的-AOF" class="headerlink" title="19.简述 Redis 的 AOF"></a>19.简述 Redis 的 AOF</h3><blockquote>
<p>AOF 通过日志，对数据的写入修改操作进行记录（<strong>读操作不记录</strong>）， <strong>只许追加文件但不可以改写文件</strong></p>
<p>这种持久化方式实时性更好</p>
<p>redis 启动之初会读取该文件重新构建数据</p>
<p>redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作</p>
<p>通过配置文件打开 AOF</p>
</blockquote>
<ul>
<li><p>AOF 触发条件</p>
<ul>
<li>手动触发：<code>bgrewriteaof</code></li>
<li>自动触发：配置文件中设置 <code>appendonly yes</code> 开启</li>
</ul>
</li>
<li><p>AOF 持久化流程</p>
<ul>
<li>客户端的请求写命令会被 append 追加到 AOF 缓冲区内</li>
<li>AOF 缓冲区根据 AOF 持久化策略 <code>[always,everysec,no]</code> 将操作 sync 同步到磁盘的 AOF 文件中</li>
<li>AOF 文件大小超过重写策略或手动重写时，会对 AOF 文件 rewrite 重写，压缩 AOF 文件容量</li>
<li>Redis 服务重启时，会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的</li>
</ul>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/AOF%E6%B5%81%E7%A8%8B.png" alt="AOF流程"></p>
</li>
</ul>
<h3 id="20-简述-AOF-的持久化策略（写入策略）"><a href="#20-简述-AOF-的持久化策略（写入策略）" class="headerlink" title="20.简述 AOF 的持久化策略（写入策略）"></a>20.简述 AOF 的持久化策略（写入策略）</h3><ul>
<li><code>always</code>：每执行一次数据修改命令就将其命令写入到磁盘日志文件上</li>
<li><code>everysec</code>：每秒将命令写入到磁盘日志文件上<strong>（默认策略）</strong></li>
<li><code>no</code>：不主动设置，由操作系统决定什么时候写入到磁盘日志文件上</li>
</ul>
<h3 id="21-简述-AOF-的重写"><a href="#21-简述-AOF-的重写" class="headerlink" title="21.简述 AOF 的重写"></a>21.简述 AOF 的重写</h3><blockquote>
<p>随着客户端不断进行操作，AOF 对应的文件也越来越大</p>
<p><code>redis</code> 提供了 <code>bgrewriteaof</code> 函数，针对目前数据库中数据，在不读取原有 AOF 文件的基础上，重写了一个新的 AOF 文件，减少文件大小</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/AOF%E9%87%8D%E5%86%99.png" alt="AOF重写"></p>
</blockquote>
<ul>
<li><p>何时触发重写</p>
<blockquote>
<p>Redis 会记录上次重写时的 AOF 大小，默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发</p>
<p>重写虽然可以节约大量磁盘空间，减少恢复时间</p>
<p>但是每次重写还是有一定的负担的，因此设定 Redis 要满足一定条件才会进行重写</p>
<ul>
<li><code>auto-aof-rewrite-percentage</code>：设置重写的基准值，文件达到 100% 时开始重写（文件是原来重写后文件的 2 倍时触发）</li>
<li><code>auto-aof-rewrite-min-size</code>：设置重写的基准值，最小文件64MB。达到这个值开始重写</li>
</ul>
<p>例如：文件达到 70MB 开始重写，降到 50MB，下次什么时候开始重写？100MB</p>
<p>系统载入时或者上次重写完毕时，Redis 会记录此时 AOF 大小，设为 base_size,</p>
<p>如果 Redis 的 AOF 当前大小 &gt;&#x3D; <code>base_size + base_size * 100%</code> （默认）且当前大小&gt;&#x3D;64mb（默认）的情况下，Redis 会对 AOF 进行重写</p>
</blockquote>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/AOF%E6%B5%81%E7%A8%8B%E5%9B%BE.png" alt="AOF流程图"></p>
<h3 id="22-RDB-与-AOF-优缺点比较"><a href="#22-RDB-与-AOF-优缺点比较" class="headerlink" title="22.RDB 与 AOF 优缺点比较"></a>22.RDB 与 AOF 优缺点比较</h3><blockquote>
<p>AOF 占用的文件体积比 RDB 大</p>
<p>一般来说利用 AOF 备份对系统的消耗比 RDB 低</p>
<p>对于备份时出现系统故障，RDB 数据可能会全丢，但 AOF 只会损失一部分</p>
<p>RDB 恢复速度比 AOF 低</p>
</blockquote>
<ul>
<li><p>RDB 总结</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/RDB%E6%80%BB%E7%BB%93.png" alt="RDB总结"></p>
<ul>
<li><p>RDB 优势</p>
<ul>
<li><p>适合大规模的数据恢复</p>
</li>
<li><p>对数据完整性和一致性要求不高更适合使用</p>
</li>
<li><p>节省磁盘空间</p>
</li>
<li><p>恢复速度快</p>
</li>
</ul>
</li>
<li><p>RDB 劣势</p>
<ul>
<li>Fork 的时候，内存中的数据被克隆了一份，大致 2 倍的膨胀性需要考虑</li>
<li>虽然 Redis 在 fork 时使用了<strong>写时拷贝技术</strong>，但是如果数据庞大时还是比较消耗性能</li>
<li><strong>在备份周期在一定间隔时间做一次备份，所以如果 Redis 意外 down 掉的话，就会丢失最后一次快照后的所有修改</strong></li>
</ul>
</li>
</ul>
</li>
<li><p>AOF 总结</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/AOF%E6%80%BB%E7%BB%93.jpg" alt="AOF总结"></p>
<ul>
<li>AOF 优势<ul>
<li>备份机制更稳健，丢失数据概率更低</li>
<li>可读的日志文本，通过操作 AOF 稳健，可以处理误操作</li>
</ul>
</li>
<li>AOF 劣势<ul>
<li>比起 RDB 占用更多的磁盘空间</li>
<li>恢复备份速度要慢</li>
<li>每次读写都同步的话，有一定的性能压力</li>
<li>存在个别 Bug，造成恢复不能</li>
</ul>
</li>
</ul>
</li>
<li><p>RDB 和 AOF 同时开启，redis 听谁的？</p>
<blockquote>
<p>AOF 和 RDB 同时开启，系统默认取 AOF 的数据（数据不会存在丢失）</p>
</blockquote>
</li>
<li><p>RDB 和 AOF 用哪个好</p>
<blockquote>
<p><strong>官方推荐两个都启用</strong></p>
<p><strong>如果对数据不敏感，可以选单独用RDB</strong></p>
<p><strong>不建议单独用 AOF，因为可能会出现Bug</strong></p>
<p><strong>如果只是做纯内存缓存，可以都不用</strong></p>
</blockquote>
</li>
<li><p><a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/118435401">Redis 持久化方案总结</a></p>
</li>
</ul>
<hr>
<h3 id="23-MySQL-与-Redis-区别"><a href="#23-MySQL-与-Redis-区别" class="headerlink" title="23.MySQL 与 Redis 区别"></a>23.MySQL 与 Redis 区别</h3><blockquote>
<p>mysql 是关系型数据库，并且其将数据存储在硬盘中，读取速度较慢</p>
<p>redis 是非关系型数据库，并且其将数据存储在内存中，读取速度较快</p>
</blockquote>
<h3 id="24-简述-Redis-过期策略"><a href="#24-简述-Redis-过期策略" class="headerlink" title="24.简述 Redis 过期策略"></a>24.简述 Redis 过期策略</h3><p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5%E5%AF%B9%E6%AF%94.png" alt="删除策略对比"></p>
<ul>
<li><p>定时删除（立即删除）<strong>（时间换空间）</strong></p>
<blockquote>
<p>Redis 不可能时时刻刻遍历所有被设置了生存时间的 key，来检测数据是否已经到达过期时间，然后对它进行删除</p>
<p>创建一个定时器，当 key 设置有过期时间，且过期时间到达时，由定时器任务立即执行对键的删除操作 </p>
<p>创建一个定时器需要用到 Redis 服务器中的时间事件，而当前时间事件的实现方式——无序链表，查找一个事件的时间复杂度为 O(N)——并不能高效地处理大量时间事件</p>
<p>因此要让服务器创建大量的定时器，从而实现定时删除策略，在现阶段来说并不现实</p>
<p>立即删除能保证内存中数据的最大新鲜度，因为它保证过期键值会在过期后马上被删除，其所占用的内存也会随之释放</p>
</blockquote>
<ul>
<li>优点：易于理解，立即删除能保证内存中数据的最大新鲜度</li>
<li>缺点：对 CPU 不友好，用处理器性能换取存储空间 。当 redis 较忙时，设置删除时间碰巧到期会增加 redis 负担</li>
</ul>
</li>
<li><p>惰性删除<strong>（空间换时间）</strong></p>
<blockquote>
<p>数据到达过期时间，不做处理。等下次访问该数据时：</p>
<p>如果未过期，返回数据 </p>
<p>发现已过期，删除并返回不存在</p>
<p>在使用惰性删除策略时，如果数据库中有非常多的过期键，而这些过期键又恰好没有被访问到的话，那么它们也许永远也不会被删除（除非用户手动执行 FLUSHDB），我们甚至可以将这种情况看作是一种<strong>内存泄漏</strong>——无用的垃圾数据占用了大量的内存，而服务器却不会自己去释放它们，这对于运行状态非常依赖于内存的 Redis 服务器来说，肯定不是一个好消息</p>
</blockquote>
<ul>
<li>优点：服务器运算资源占用小</li>
<li>缺点：容易造成某些数据长期霸占在内存中，不被删除的情况</li>
</ul>
</li>
<li><p>定期删除</p>
<blockquote>
<p>定期删除策略是前两种策略的折中</p>
<p>定期删除策略每隔一段时间执行一次删除过期键操作，并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响</p>
<p>周期性轮询 redis 库中的时效性数据，采用随机抽取的策略，利用过期数据占比的方式控制删除频度</p>
<p>redis 默认每个 100ms 检查，是否有过期的 key，有过期 key 则删除</p>
<p>注意：redis 不是每隔 100ms 将所有的 key 检查一次而是随机抽取进行检查</p>
<p>因此，如果只采用定期删除策略，会导致很多 key 过期但没有删除</p>
</blockquote>
<ul>
<li><p>特点</p>
<blockquote>
<p>CPU性能占用设置有峰值，检测频度可自定义设置</p>
<p>内存压力不是很大，长期占用内存的冷数据会被持续清理</p>
<p>总结：<strong>周期性抽查存储空间 （随机抽查，重点抽查）</strong></p>
</blockquote>
</li>
<li><p>定期删除策略的难点：<strong>确定删除操作执行的时长和频率</strong></p>
<ul>
<li>如果删除操作执行得太频繁，或者执行的时间太长，定期删除策略就会退化成立即删除策略，以至于将 CPU 时间过多地消耗在删除过期键上面</li>
<li>如果删除操作执行得太少，或者执行的时间太短，定期删除策略又会和惰性删除束略一样，出现浪费内存的情况</li>
</ul>
<blockquote>
<p>因此如果采用定期删除策略的话，服务器必须根据情况，合理地设置删除操作的执行时长和执行频率</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="25-数据库与缓存不一致问题"><a href="#25-数据库与缓存不一致问题" class="headerlink" title="25.数据库与缓存不一致问题"></a>25.数据库与缓存不一致问题</h3><ol>
<li><p>定义</p>
<blockquote>
<p><strong>缓存不一致一般指代缓存最终不一致</strong>，即在业务执行增删改查过程，一定时间后，缓存中的数据和数据库存储数据发生不一致的情况</p>
<p><strong>缓存做不到绝对一致性，但可以做到最终一致性</strong></p>
</blockquote>
<ul>
<li>低一致性需求：使用 Redis 自带的内存淘汰机制</li>
<li>高一致性需求：主动更新，并以超时剔除作为兜底方案</li>
</ul>
</li>
<li><p>缓存读写策略（以下三种策略都属于主动更新策略）</p>
<ul>
<li><p><strong>Cache Aside（旁路缓存）</strong></p>
<blockquote>
<p><strong>是平时使用比较多的一个缓存读写模式，比较适合读请求比较多的场景</strong></p>
<p>当缓存的数据有更新值了，它采用的不是更新缓存数据，而是删除缓存数据</p>
</blockquote>
<ul>
<li>读步骤<ul>
<li>从 cache 中读取数据，读取到就直接返回</li>
<li>cache 中读取不到的话，就从 DB 中读取数据返回</li>
<li>再把数据放到 cache 中，并设置超时时间</li>
</ul>
</li>
<li>写步骤<ul>
<li>先更新 DB</li>
<li>然后直接删除 cache</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Read&#x2F;Write Through（读写穿透）</strong></p>
<blockquote>
<p>把 cache 视为主要数据存储，从中读取数据并将数据写入其中</p>
<p>cache 服务负责将此数据读取和写入 DB，从而减轻了应用程序的职责</p>
<p><strong>当缓存的数据有更新值了，它采用的是更新缓存数据，然后 cache 服务自己更新 DB（同步更新 cache 和 DB）</strong></p>
</blockquote>
<ul>
<li>读步骤<ul>
<li>从 cache 中读取数据，读取到就直接返回 </li>
<li>读取不到的话，先从 DB 加载，写入到 cache 后返回响应</li>
</ul>
</li>
<li>写步骤<ul>
<li>先查 cache，cache 中不存在，直接更新 DB</li>
<li>cache 中存在，则先更新 cache，然后 cache 服务自己更新 DB<strong>（同步更新 cache 和 DB）</strong></li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Write Behind Pattern（异步缓存写入）</strong></p>
<blockquote>
<p><code>Write Behind Pattern</code> 和 <code>Read/Write Through Pattern</code> 很相似，两者都是由 cache 服务来负责 cache 和 DB 的读写</p>
<p>但是两个又有很大的不同：<code>Read/Write Through</code> 是同步更新 cache 和 DB，而 <code>Write Behind Caching </code>则是只更新缓存，不直接更新 DB，而是改为异步批量的方式来更新 DB</p>
</blockquote>
<ul>
<li>读步骤<ul>
<li>从 cache 中读取数据，读取到就直接返回 </li>
<li>读取不到的话，先从 DB 加载，写入到 cache 后返回响应</li>
</ul>
</li>
<li>写步骤<ul>
<li>先查 cache，cache 中不存在，直接更新 DB</li>
<li>cache 中存在，则先更新 cache，然后批量更新 DB<strong>（异步更新 cache 和 DB）</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><a target="_blank" rel="noopener" href="https://blog.csdn.net/v123411739/article/details/124237900">缓存不一致的解决方法</a></p>
<ul>
<li><p><strong>先更新数据库值，再更新缓存值</strong></p>
<blockquote>
<p>不适用于<code>写多读少</code>的情况</p>
<p>可能出现如下不一致</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%8F%8C%E6%9B%B4%E6%96%B0.png" alt="双更新"></p>
</li>
<li><p><strong>先更新数据库值，再删除缓存值</strong></p>
<blockquote>
<p>更新数据库操作耗时远大于删除缓存值操作，发生不一致情况概率较低</p>
<p>可能出现如下不一致</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%85%88%E6%9B%B4%E5%86%8D%E5%88%A0.png" alt="先更再删"></p>
</li>
<li><p><strong>先删除缓存值，再更新数据库值</strong></p>
<blockquote>
<p>更新数据库操作耗时远大于删除缓存值操作，发生不一致情况概率较高</p>
<p>可能出现如下不一致</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%85%88%E5%88%A0%E5%86%8D%E6%9B%B4.png" alt="先删再更"></p>
</li>
<li><p><strong>采用延时双删策略（双淘汰策略）</strong></p>
<ul>
<li>先删除缓存</li>
<li>再写数据库</li>
<li>休眠一定时间，再次删除缓存</li>
</ul>
<blockquote>
<p>难以确定休眠时间，设置过长效率低；设置过短则无法保证一致（主从库未同步）</p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%8F%8C%E5%88%A0.png" alt="双删"></p>
</blockquote>
</li>
<li><p><strong>设置缓存过期时间</strong></p>
<blockquote>
<p>给缓存设置过期时间，是保证最终一致性的解决方案</p>
<p>所有的写操作以数据库为准，只要到达缓存过期时间，则后面的读请求自然会从数据库中读取新值然后回填缓存</p>
</blockquote>
</li>
<li><p><strong>异步更新缓存(基于订阅binlog的同步机制)</strong></p>
<blockquote>
<p>MySQL binlog 增量订阅消费 + 消息队列 + 增量数据更新到 redis</p>
<p>读 Redis：热数据基本都在 Redis</p>
<p>写 MySQL：增删改都是操作 MySQL</p>
<p>更新 Redis 数据：MySQL 的数据操作 binlog，来更新到 Redis</p>
<p>读取 binlog 后分析，利用消息队列，推送更新各台的 redis 缓存数据</p>
<p>这样一旦 MySQL 中产生了新的写入、更新、删除等操作，就可以把 binlog 相关的消息推送至 Redis，Redis 再根据 binlog 中的记录，对 Redis 进行更新</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/binlog.png" alt="binlog"></p>
<blockquote>
<p>存在问题：灰度切读（逐步把读取操作从原数据库转移到新数据库）时无法保证一致性</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E7%81%B0%E5%BA%A6%E5%88%87%E8%AF%BB.png" alt="灰度切读"></p>
</blockquote>
</li>
<li><p><strong>最终方案：缓存三重删除 + 数据一致性校验 + 更新流程禁用缓存 + 强制读Redis主节点</strong></p>
<ul>
<li><p>更新数据库同步删除缓存</p>
</li>
<li><p>监听数据库的 binlog 异步删除缓存：带重试，保障一定会最终删除成功</p>
</li>
<li><p>缓存数据带过期时间，过期后自动删除，越近更新的数据过期时间越短</p>
<ul>
<li><p>主要用于进一步防止并发下的脏数据问题</p>
</li>
<li><p>解决一些由于未知情况，导致需要更换缓存结构的问题</p>
</li>
</ul>
</li>
<li><p>监听数据库的 binlog 延迟 N 秒后进行数据一致性校验</p>
<ul>
<li>解决一些极端场景下的脏数据问题</li>
</ul>
</li>
<li><p>存在数据库更新的链路禁用对应缓存</p>
<ul>
<li>防止并发下短期内的脏数据影响到更新流程</li>
</ul>
</li>
<li><p>强制读 Redis 主节点</p>
</li>
<li><p>查询异步数据一致性校验、灰度放量</p>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E6%9C%80%E7%BB%88%E6%96%B9%E6%A1%88.png" alt="最终方案"></p>
</li>
</ul>
</li>
</ol>
<h3 id="26-缓存淘汰策略"><a href="#26-缓存淘汰策略" class="headerlink" title="26.缓存淘汰策略"></a>26.缓存淘汰策略</h3><ul>
<li><p><code>noeviction</code>：不会驱逐任何 key，内存不够使用时，对申请内存的命令报错</p>
</li>
<li><p><strong><code>allkeys-lru</code>（常用）：对所有 key 使用 LRU 算法进行删除</strong></p>
</li>
<li><p><code>volatile-lru</code>：对所有设置了过期时间的 key 使用 LRU 算法进行删除</p>
</li>
<li><p><code>allkeys-random</code>：对所有 key 随机删除</p>
</li>
<li><p><code>volatile-random</code>：对所有设置了过期时间的 key 随机删除</p>
</li>
<li><p><code>volatile-ttl</code>：删除马上要过期的 key</p>
</li>
<li><p><code>allkeys-lfu</code>：对所有 key 使用 LFU 算法进行删除</p>
</li>
<li><p><code>volatile-lfu</code>：对所有设置了过期时间的 key 使用 LFU 算法进行删除</p>
</li>
</ul>
<blockquote>
<p>总结</p>
<p>2 个维度：所有 key、有设置过期时间的 key</p>
<p>4 个策略：LRU、LFU、random、ttl</p>
<p>以 volatile 开头的策略只针对设置了过期时间的数据，即使缓存没有被写满，如果数据过期也会被删除</p>
<p>以 allkeys 开头的策略是针对所有数据的，如果数据被选中了，即使过期时间没到，也会被删除。当然如果它的过期时间到了但未被策略选中，同样会被删除</p>
</blockquote>
<ul>
<li><p><strong>经典 LRU、LFU</strong></p>
<ul>
<li><p>最近最少使用 LRU</p>
<blockquote>
<p>高效实现 LRU：双向链表 + 哈希表</p>
<ul>
<li><p>双向链表用于组织数据，最常访问的排在前面</p>
</li>
<li><p>哈希表用于存储 key 和 value 链表节点</p>
</li>
</ul>
</blockquote>
</li>
<li><p>最不经常使用 LFU</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/LFU.png" alt="LFU"></p>
</li>
</ul>
</li>
<li><p><strong>Redis 实现 LRU、LFU</strong></p>
<ul>
<li><p>最近最少使用 LRU（默认采用该淘汰策略）</p>
<blockquote>
<p>从 LRU 的实现上来讲，要么额外存储一个访问时间字段，要么将所有 key 做成链表，每次读取key，对链表做重新排序</p>
<p>原生的 LRU 用的就是第二种消耗大量的额外的内存，且需要大量链表操作和排序</p>
<p>而 Redis 中的实现则很简单，类似于第一种，在现有数据结构的基础上使用随机采样法来淘汰元素，能达到和 LRU 算法非常近似的效果</p>
<p><strong>Redis 为实现近似 LRU 算法，它给每个 key 增加了一个额外的小字段，这个字段的长度是 24 个 bit，也就是最后一次被访问的时间戳</strong></p>
<p>它的处理方式只有懒惰处理。当 Redis 执行写操作时，发现内存超出 maxmemory（想到要阈值，可以通过参数配置），就会执行一次LRU 淘汰算法</p>
<p><strong>这个淘汰是随机的，随机采样出 5（默认5，可以通过参数配置)）个 key，然后淘汰掉最旧的 key，如果淘汰后内存还是超出 maxmemory，那就继续随机采样淘汰，直到内存低于maxmemory 为止</strong></p>
<p>而采样范围就是看 maxmemory-policy 选择的是 allkeys 还是 volatile</p>
<p>但这种随机性肯定会带来一个局部性问题，举例来说就是现在有 10 个 key，由新到旧分别是1,2,3,4,5,6,7,8,9,10，如果现在要淘汰三个，严格的 LRU 应该淘汰 8,9,10，但 redis 可能存在取样取出来 1,2,3,4,5，此时只能淘汰 5</p>
<p>这样的随机性可能带来淘汰的不严格，但因为实现简单，不会待读写key造成太大硬性，还是可以接受</p>
<p><strong>而且在 Redis 3.0 中对近似 LRU 还做了一点小优化，也就是增加了一个数组，叫淘汰池（eviction poo，大小是 maxmemory_samples，可设置）</strong></p>
<p>在每一次淘汰循环中，新随机出来的 key 列表会和淘汰池中的 key 列表进行比较和排序，淘汰掉最旧的一个 key 之后，保留剩余较旧的 key 列表放入淘汰池中留待下一个循环</p>
<p>因为随机性可能会使得淘汰的数据过新，上一次淘汰的数远比这次淘汰的数新，使用淘汰池将这个问题解决。注：淘汰池中的 key 并未清除，仍可读取</p>
</blockquote>
</li>
<li><p>最不经常使用 LFU</p>
<blockquote>
<p>LFU 即最不经常使用页置换算法，要求在页置换时置换引用计数最小的页，因为经常使用的页应该有一个较大的引用次数。所以要存储两个额外字段，一个是时间区间，一个是访问次数</p>
<p>Redis 在 4.0 以后支持，Redis 使用使用 24bit 存储：其中 16bit 用于表示上一次递减时间 （时间戳的后16位），8bit 表示访问次数</p>
<p>注：8 位只能代表 255，但是 redis 并没有采用线性上升的方式，而是通过一个复杂的公式，通过配置两个参数来调整数据的递增速度</p>
<p>如在影响因子 lfu-log-factor 为 10 的情况下，经过 1 百万次命中才能达到 255</p>
<p>每次读取 key 会检查这 24bit，根据当前时间戳 key 中那 24bit 存的时间的差值，如果距离现在超过 N 分钟（可配置 lfu-decay-time），则递减或者减半，然后通过衰减后的 24bit 通过<code>LFULogIncr()</code> 重新计算后更新这24bit。具体如何减少见代码</p>
</blockquote>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">updateLFU</span><span class="params">(robj *val)</span> &#123;</span><br><span class="line">    <span class="comment">//衰减</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> counter = LFUDecrAndReturn(val);</span><br><span class="line">    <span class="comment">//根据衰减后的counter重新计算新的counter</span></span><br><span class="line">    counter = LFULogIncr(counter);</span><br><span class="line">    val-&gt;lru = (LFUGetTimeInMinutes()&lt;&lt;<span class="number">8</span>) | counter; </span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="type">unsigned</span> <span class="type">long</span> <span class="title function_">LFUDecrAndReturn</span><span class="params">(robj *o)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> ldt = o-&gt;lru &gt;&gt; <span class="number">8</span>;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> counter = o-&gt;lru &amp; <span class="number">255</span>; </span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : <span class="number">0</span>;</span><br><span class="line">    <span class="comment">//server.lfu_decay_time默认为1,每经过一分钟counter衰减1</span></span><br><span class="line">    <span class="keyword">if</span> (num_periods)</span><br><span class="line">        counter = (num_periods &gt; counter) ? <span class="number">0</span> : counter - num_periods;<span class="comment">//如果需要衰减,则计算衰减后的值</span></span><br><span class="line">    <span class="keyword">return</span> counter;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">LFULogIncr</span><span class="params">(<span class="type">uint8_t</span> counter)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (counter == <span class="number">255</span>) <span class="keyword">return</span> <span class="number">255</span>;<span class="comment">//counter最大只能存储到255,到达后不再增加</span></span><br><span class="line">    <span class="type">double</span> r = (<span class="type">double</span>)rand()/RAND_MAX;<span class="comment">//算一个随机的小数值</span></span><br><span class="line"><span class="comment">//新加入的key初始counter设置为LFU_INIT_VAL,默认5</span></span><br><span class="line">    <span class="type">double</span> baseval = counter - LFU_INIT_VAL;</span><br><span class="line">    <span class="keyword">if</span> (baseval &lt; <span class="number">0</span>) baseval = <span class="number">0</span>;</span><br><span class="line">    <span class="type">double</span> p = <span class="number">1.0</span>/(baseval*server.lfu_log_factor+<span class="number">1</span>);<span class="comment">//server.lfu_log_facotr默认为10</span></span><br><span class="line"><span class="comment">//counter越大,则p越小，随机值r小于p的情况就越少，counter增加得越来越慢</span></span><br><span class="line">    <span class="keyword">if</span> (r &lt; p) counter++;</span><br><span class="line">    <span class="keyword">return</span> counter;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="type">unsigned</span> <span class="type">long</span> <span class="title function_">LFUGetTimeInMinutes</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> (server.unixtime/<span class="number">60</span>) &amp; <span class="number">65535</span>;<span class="comment">//获取分钟级别的时间戳</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>LFU 随着分钟数对 counter 做衰减是基于一个原理：过去被大量访问的 key 不一定现在仍然会被访问。相当于除了计数，给时间也增加了一定的权重进行递减</strong></p>
<p>而默认情况下新写的 key 的后 8 位计数器的值为 5（可配置），防止因为访问频率过低而直接被删除</p>
<p>删除时还是随机的，随机采样 N 个key（与LRU一样），淘汰 25bit 最小的</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="27-布隆过滤器"><a href="#27-布隆过滤器" class="headerlink" title="27.布隆过滤器"></a>27.布隆过滤器</h3><ol>
<li><p>简介</p>
<blockquote>
<p>由一个初值都为零的bit数组和多个哈希函数构成，用来快速判断某个数据是否存在</p>
<p>本质就是判断具体数据存不存在一个大的集合中</p>
<p><strong>布隆过滤器是一种类似 set 的数据结构，只是统计结果不太准确</strong></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81.png" alt="布隆过滤器初始状态"></p>
</blockquote>
</li>
<li><p>特点</p>
<blockquote>
<p>高效地插入和查询，占用空间少，返回的结果是不确定性的（<strong>有不一定有，无肯定无</strong>）</p>
<p><strong>一个元素如果判断结果为存在的时候元素不一定存在，但是判断结果为不存在的时候则一定不存在</strong></p>
<p>布隆过滤器可以添加元素，但是不能删除元素。因为删掉元素会导致误判率增加</p>
<p><strong>误判只会发生在过滤器没有添加过的元素，对于添加过的元素不会发生误判</strong></p>
</blockquote>
</li>
<li><p>适用场景</p>
<ul>
<li><p>解决缓存穿透问题</p>
<blockquote>
<p>把已存在数据的 key 存在布隆过滤器中，相当于 redis 前面挡着一个布隆过滤器</p>
<p>当有新的请求时，先到布隆过滤器中查询是否存在：</p>
<p>如果布隆过滤器中不存在该条数据则直接返回</p>
<p>如果布隆过滤器中已存在，才去查询缓存 redis，如果 redis 里没查询到则穿透到 Mysql 数据库</p>
</blockquote>
</li>
<li><p>黑名单校验</p>
<blockquote>
<p>发现存在黑名单中的，就执行特定操作</p>
<p>比如：识别垃圾邮件，只要是邮箱在黑名单中的邮件，就识别为垃圾邮件</p>
<p>假设黑名单的数量是数以亿计的，存放起来就是非常耗费存储空间的，布隆过滤器则是一个较好的解决方案</p>
<p>把所有黑名单都放在布隆过滤器中，在收到邮件时，判断邮件地址是否在布隆过滤器中即可</p>
</blockquote>
</li>
</ul>
</li>
<li><p>布隆过滤器实现原理</p>
<ul>
<li><p>实现原理</p>
<blockquote>
<p>布隆过滤器（Bloom Filter）是一种专门用来解决去重问题的高级数据结构</p>
<p>实质就是一个大型位数组和几个不同的无偏 hash 函数（无偏表示分布均匀）</p>
<p>由一个初值都为零的 bit 数组和多个个哈希函数构成，用来快速判断某个数据是否存在</p>
<p>但是跟 HyperLogLog 一样，它也一样有那么一点点不精确，也存在一定的误判概率</p>
</blockquote>
<ul>
<li><p>添加 key 时</p>
<blockquote>
<p>使用多个 hash 函数对 key 进行 hash 运算得到一个整数索引值，对位数组长度进行取模运算得到一个位置</p>
<p><strong>每个 hash 函数都会得到一个不同的位置，将这几个位置都置 1 就完成了 add 操作</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8add.png" alt="布隆过滤器add"></p>
</li>
<li><p>查询 key 时</p>
<blockquote>
<p>向布隆过滤器查询某个 key 是否存在时，先把这个 key 通过相同的多个 hash 函数进行运算，查看对应的位置是否都为 1</p>
<p><strong>只要有其中一位是零就表示这个 key 不存在，但如果都是 1，则不一定存在对应的 key</strong></p>
<p>因为存在 hash 冲突，这些位置的 1 可能是因为其他的 key 存在导致的</p>
</blockquote>
</li>
</ul>
</li>
<li><p>布隆过滤器误判率，为什么不要删除</p>
<blockquote>
<p>布隆过滤器的误判是指多个输入经过哈希之后在相同的 bit 位置 1 了，这样就无法判断究竟是哪个输入产生的</p>
<p>这种情况也造成了布隆过滤器的删除问题，因为布隆过滤器的每一个 bit 并不是独占的，很有可能多个元素共享了某一位。如果直接删除这一位的话，会影响其他的元素</p>
<p><strong>布隆过滤器可以添加元素，但是不能删除元素。因为删掉元素会导致误判率增加</strong></p>
</blockquote>
</li>
</ul>
</li>
<li><p>布隆过滤器总结</p>
<blockquote>
<p><strong>使用时最好不要让实际元素数量远大于初始化数量</strong></p>
<p>当实际元素数量超过初始化数量时，应该对布隆过滤器进行重建，重新分配一个 size 更大的过滤器，再将所有的历史元素批量 add 进行</p>
<p>布隆过滤器的优化版——<a target="_blank" rel="noopener" href="https://www.cnblogs.com/zhaodongge/p/15067657.html">布谷鸟过滤器</a></p>
</blockquote>
<ul>
<li>布隆过滤器优点：高效地插入和查询，占用空间少</li>
<li>布隆过滤器缺点：存在误判、不能删除元素</li>
</ul>
</li>
</ol>
<h3 id="28-设置-redis-内存"><a href="#28-设置-redis-内存" class="headerlink" title="28.设置 redis 内存"></a>28.设置 redis 内存</h3><blockquote>
<p>一般推荐Redis设置内存为最大物理内存的四分之三</p>
</blockquote>
<ol>
<li><p>查看 Redis 最大占用内存</p>
<blockquote>
<p>打开redis配置文件，设置 maxmemory 参数，maxmemory 是 bytes 字节类型，注意转换</p>
</blockquote>
</li>
<li><p>Redis 默认内存</p>
<blockquote>
<p>如不设置最大内存大小或设置最大内存大小为 0，则在 64 位操作系统下不限制内存大小，在 32 位操作系统下最多使用 3GB 内存</p>
</blockquote>
</li>
</ol>
<h3 id="29-Redis-大-key-问题"><a href="#29-Redis-大-key-问题" class="headerlink" title="29.Redis 大 key 问题"></a>29.Redis 大 key 问题</h3><ol>
<li>定义（多种情况）<ul>
<li>一般情况下认为字符串类型的 key 的 value 值超过 <code>10kb</code>，就算大 key</li>
<li>单个简单的 key 存储的 value 很大</li>
<li>hash， set，zset，list 中存储过多的元素</li>
<li>一个集群存储了上亿的 key</li>
</ul>
</li>
<li>大 key 的危害<ul>
<li>内存空间不均匀</li>
<li>操作耗时，存在阻塞风险</li>
<li>网络阻塞，每次获取大 key 产生的网络流量较大</li>
</ul>
</li>
<li><a target="_blank" rel="noopener" href="https://help.aliyun.com/document_detail/353223.html">大 key 的发现与解决方案</a></li>
</ol>
<h3 id="30-跳表详解"><a href="#30-跳表详解" class="headerlink" title="30.跳表详解"></a>30.<a target="_blank" rel="noopener" href="https://blog.csdn.net/qq_36389060/article/details/123955761">跳表详解</a></h3><ol>
<li><p>定义</p>
<blockquote>
<p>跳表是在链表的基础上，增加了多级索引，通过索引位置的几个跳转，实现数据的快速定位</p>
<p><strong>空间换时间</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E8%B7%B3%E8%A1%A8.png" alt="跳表"></p>
</li>
<li><p>为什么用跳跃表</p>
<blockquote>
<p>Redis 使用跳跃表作为 Zset 的底层实现之一</p>
<p>如果一个有序集合包含的 <code>元素数量比较多</code>，又或者有序集合中元素的 <code>成员是比较长的字符串时</code>, Redis 就会使用跳跃表来作为 Zset 的底层实现</p>
<p><strong>跳跃表在链表的基础上增加了多级索引以提升查找的效率，是一个空间换时间的方案，但必然会带来一个问题——索引是占内存的</strong></p>
<p>原始链表中存储的有可能是很大的对象，而索引结点只需要存储关键值值和几个指针，并不需要存储对象，因此当节点本身比较大或者元素数量比较多的时候，其优势必然会被放大，而缺点则可以忽略</p>
</blockquote>
<blockquote>
<p>Redis 的跳跃表由 <code>zskiplistNode</code> 和 <code>skiplist</code> 两个结构定义</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E8%B7%B3%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt="跳表底层结构"></p>
</blockquote>
<ul>
<li><p><code>zskiplist</code> 结构则用于保存跳跃表节点的相关信息（比如表头节点、表尾节点、长度）</p>
<ul>
<li><code>header</code>：指向跳跃表的表头节点，通过这个指针程序定位表头节点的时间复杂度就为O(1)</li>
<li><code>tail</code>：指向跳跃表的表尾节点，通过这个指针程序定位表尾节点的时间复杂度就为O(1)</li>
<li><code>level</code>：记录目前跳跃表内，层数最大的那个节点的层数（<strong>表头节点的层数不计算在内</strong>），通过这个属性可以在 O(1) 的时间复杂度内获取层高最高的节点的层数</li>
<li><code>length</code>：记录跳跃表的长度，也即是跳跃表目前包含节点的数量（<strong>表头节点不计算在内</strong>），通过这个属性可以在以 O(1)  的时间复杂度内返回跳跃表的长度</li>
</ul>
</li>
<li><p><code>zskiplistNode</code> 结构用于表示跳跃表节点</p>
<ul>
<li><p>层(level)：节点中用 L1、L2、L3 等字样标记节点的各个层，L1代表一级索引，以此类推</p>
<blockquote>
<p>每个层都带有两个属性：前进指针和跨度</p>
<p>在上图中，连线上带有数字的箭头就代表前进指针 ，而那个数字就是跨度</p>
<p>当程序从表头向表尾进行遍历时，访问会沿着层的前进指针进行</p>
<p><strong>前进指针用于访问位于表尾方向的其他节点</strong></p>
<p><strong>跨度则记录了前进指针所指向节点和当前节点的距离（跨度越大、距离越远）</strong></p>
<p>每次创建一个新跳跃表节点的时候，程序都根据幂次定律（越大的数出现的概率越小）随机生成一个介于 1 和 32 之间的值作为 level 数组的大小，这个大小就是层的<code>高度</code></p>
</blockquote>
</li>
<li><p>后退(backward)指针：节点中用 BW 字样标记节点的后退指针，它指向位于当前节点的前一个节点</p>
<blockquote>
<p><strong>后退指针在程序从表尾向表头遍历时使用</strong></p>
<p>与前进指针所不同的是每个节点只有一个后退指针，因此每次只能后退一个节点</p>
</blockquote>
</li>
<li><p>分值(score)：各个节点中的 1.0、2.0、3.0 是节点所保存的分值</p>
<blockquote>
<p>在跳跃表中，节点按各自所保存的分值从小到大排列</p>
</blockquote>
</li>
<li><p>成员对象(oj)：各个节点中的o1、o2和o3是节点所保存的成员对象</p>
<blockquote>
<p>在同一个跳跃表中，<strong>各个节点保存的成员对象必须是唯一的</strong>，但是<strong>多个节点保存的分值却可以是相同的</strong></p>
<p><strong>分值相同的节点将按照成员对象在字典序中的大小来进行排序</strong>，成员对象较小的节点会排在前面（靠近表头的方向），而成员对象较大的节点则会排在后面（靠近表尾的方向）</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>跳跃表操作的时间复杂度</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E8%B7%B3%E8%A1%A8%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.png" alt="跳表时间复杂度"></p>
</li>
<li><p>为什么是跳表，而不是<code>红黑树、B+树</code></p>
<ul>
<li><p>为什么不用红黑树</p>
<blockquote>
<p>红黑树的插入、删除、查找以及迭代输出有序序列这几个操作时间复杂度跟跳表一样</p>
<p>但按照区间来查找数据这个操作红黑树的效率没有跳表高， 跳表可以做到 O(logn) 的时间复杂度定位区间的起点，再在原始链表中顺序往后遍历，非常高效</p>
<p>跳表更容易代码实现</p>
<p>跳表更加灵活，它可以通过改变索引构建策略，有效平衡执行效率和内存消耗</p>
</blockquote>
<blockquote>
<p>跳表不能完全替代红黑树，红黑树比跳表的出现要早一些，很多编程语言中的 Map 类型都是通过红黑树来实现，做业务开发时可以直接拿来用，不用费劲自己去实现，<strong>但跳表没有现成的实现，所以在开发中如果想使用跳表必须要自己实现（跳表缺点！！！）</strong></p>
</blockquote>
</li>
<li><p>为什么不用 B+ 树</p>
<blockquote>
<p>B+ 树的原理是叶子节点存储数据，非叶子节点存储索引，B+ 树的每个节点可以存储多个关键字，它将节点大小设置为磁盘页的大小，充分利用了磁盘预读的功能</p>
<p>每次读取磁盘页时就会读取一整个节点，每个叶子节点还有指向前后节点的指针，为的是最大限度的降低磁盘的 IO。因为数据在内存中读取耗费的时间是从磁盘的 IO 读取的百万分之一</p>
<p><strong>而Redis是内存中读取数据，不涉及 IO，因此使用了跳表</strong></p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h3 id="31-MySQL-里有-2000w-数据，Redis-中只存-20w-的数据，如何保证Redis中的数据都是热点数据"><a href="#31-MySQL-里有-2000w-数据，Redis-中只存-20w-的数据，如何保证Redis中的数据都是热点数据" class="headerlink" title="31.MySQL 里有 2000w 数据，Redis 中只存 20w 的数据，如何保证Redis中的数据都是热点数据?"></a>31.MySQL 里有 2000w 数据，Redis 中只存 20w 的数据，如何保证Redis中的数据都是热点数据?</h3><ol>
<li><p><strong>保留热点数据</strong></p>
<blockquote>
<p>对于保留 Redis 热点数据来说，我们可以使用 Redis 的内存淘汰策略来实现</p>
<p>可以使用 <strong>allkeys-lru淘汰策略</strong>，该淘汰策略是从 Redis 的数据中挑选最近最少使用的数据删除，这样频繁被访问的数据就可以保留下来了</p>
</blockquote>
</li>
<li><p><strong>保证 Redis 只存 20w 的数据</strong></p>
<blockquote>
<p>1 个中文占 2 个字节，假如 1 条数据有 100 个中文，则 1 条数据占 200 字节，20w 数据乘以 200字节等于 4000w 字节（大概等于38M）</p>
<p>所以要保证能存 20w 数据，Redis 需要 38M 的内存</p>
<p>设置 Redis 内存为 38M</p>
</blockquote>
</li>
</ol>
<h3 id="32-Redis存储对象信息是用Hash还是String好？"><a href="#32-Redis存储对象信息是用Hash还是String好？" class="headerlink" title="32.Redis存储对象信息是用Hash还是String好？"></a>32.Redis存储对象信息是用Hash还是String好？</h3><ol>
<li><p>Redis 存储底层结构</p>
<blockquote>
<p>Redis 内部使用一个 RedisObject 对象来表示所有的 key 和 value，RedisObject 中的 type，则是代表一个 value 对象具体是何种数据类型，它包含字符串（String）、链表（List）、哈希结构（Hash）、集合（Set）、有序集合（Sorted set）</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/redisObject.jpg" alt="redisObject"></p>
</li>
<li><p>String</p>
<blockquote>
<p>String 数据结构是简单的 key-value 类型，value 其实不仅是 String，也可以是数字</p>
<p>Redis 中的 String 可以表示很多语义：</p>
<ul>
<li>字符串（bits）</li>
<li>整数</li>
<li>浮点数</li>
</ul>
<p>这三种类型，Redis 会根据具体的场景完成自动转换，并且根据需要选取底层的承载方式</p>
<p>String 在Redis 内部存储默认就是一个字符串，被 RedisObject 所引用，当遇到 incr、decr 等操作时会转成数值型进行计算，此时 RedisObject 的 encoding 字段为int</p>
</blockquote>
<blockquote>
<p>由于 Redis 的字符串是动态字符串，可以修改，内部结构类似于 Java 的 ArrayList，采用预分配冗余空间的方式来减少内存的频繁分配（内部为当前字符串实际分配的空间 capacity，一般高于实际字符串长度 len）</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/string%E5%86%97%E4%BD%99%E5%88%86%E9%85%8D.jpg" alt="string冗余分配"></p>
<p>在存储过程中，可以将用户信息使用 Json 序列化成字符串，然后将序列化后的字符串存入 Redis 进行缓存</p>
</blockquote>
</li>
<li><p>Hash</p>
<blockquote>
<p>Hash 在很多编程语言中都有着很广泛的应用，而在 Redis 中也是如此。在 Redis 中，Hash 常常用来缓存一些对象信息，如用户信息、商品信息、配置信息等，因此也被称为字典（dictionary）</p>
<p>Redis 的字典使用 Hash table 作为底层实现， 一个 Hash table 里面可以有多个哈希表节点，而每个哈希表节点保存了字典中的一个键值对</p>
<p>实际上，Redis 数据库底层也是采用 Hash table 来存储键值对的</p>
<p><strong>Redis 的 Hash 相当于 Java 的 HashMap，内部结构实现与 HashMap 一致，即数组+链表结构。只是 reHash 方式不一样（<a target="_blank" rel="noopener" href="https://juejin.cn/post/7027757834943234085">Redis 采用渐进式 reHash</a>）</strong></p>
</blockquote>
</li>
<li><p>总结</p>
<ul>
<li><strong>适合用 String 存储的情况</strong><ul>
<li>每次需要访问大量的字段</li>
<li>存储的结构具有多层嵌套的时候</li>
</ul>
</li>
<li><strong>适合用 Hash 存储的情况</strong><ul>
<li>在大多数情况中只需要访问少量字段</li>
<li>自己始终知道哪些字段可用，防止使用 mget 时获取不到想要的数据</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="33-Redis-的-IO-多路复用如何理解，为什么单线程还可以抗那么高的-QPS-Query-Per-Second"><a href="#33-Redis-的-IO-多路复用如何理解，为什么单线程还可以抗那么高的-QPS-Query-Per-Second" class="headerlink" title="33.Redis 的 IO 多路复用如何理解，为什么单线程还可以抗那么高的 QPS(Query Per Second)"></a>33.Redis 的 IO 多路复用如何理解，为什么单线程还可以抗那么高的 QPS(Query Per Second)</h3><ol>
<li><p>IO 多路复用简单说明</p>
<blockquote>
<p>I&#x2F;O 的读和写本身是堵塞的，比如当 socket 中有数据时，Redis 会通过调用先将数据从内核态空间拷贝到用户态空间，再交给 Redis 调用</p>
<p>而这个拷贝的过程就是阻塞的，当数据量越大时拷贝所需要的时间就越多，而这些操作都是基于单线程完成的</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E8%80%97%E6%97%B6%E7%9A%84IO%E6%93%8D%E4%BD%9C.png" alt="耗时的IO操作"></p>
<blockquote>
<p>在 Redis 6.0 中新增了多线程的功能来提高 I&#x2F;O 的读写性能</p>
<p>主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行，这样就可以使多个 socket 的读写可以并行化了</p>
<p>采用多路 I&#x2F;O 复用技术可以让单个线程高效的处理多个连接请求（尽量减少网络IO的时间消耗），将最耗时的Socket的读取、请求解析、写入单独外包出去，剩下的命令执行仍然由主线程串行执行并和内存的数据交互</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%A4%9A%E4%B8%AAIO%E7%BA%BF%E7%A8%8B.png" alt="多个IO线程"></p>
<blockquote>
<p>结合上图可知，网络IO操作就变成多线程化了，其他核心部分仍然是线程安全的，是个不错的折中办法</p>
<p><strong>Redis 6.0 将网络数据读写、请求协议解析通过多个 IO 线程的来处理 ，对于真正的命令执行来说，仍然使用主线程操作，一举两得</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E5%A4%9A%E7%BA%BF%E7%A8%8B.png" alt="多线程"></p>
</li>
<li><p><a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1r54y1f7bU">IO 多路复用三种机制 Select、Poll、Epoll</a></p>
</li>
</ol>
<h3 id="34-Redis-分布式锁-RedLock"><a href="#34-Redis-分布式锁-RedLock" class="headerlink" title="34.Redis 分布式锁 RedLock"></a>34.<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1XF411g7ih">Redis 分布式锁 RedLock</a></h3><ol>
<li><p>前奏：Redis 分布式锁 <code>setnx</code></p>
<blockquote>
<p><code>setnx</code> 即 <code>set if not exist</code></p>
<p>用 setnx 争抢锁，再用 expire 给锁加一个过期时间以防止忘记释放锁</p>
<p>如果在 setnx 后执行 expire 时意外宕机如何处理？</p>
<p>set 指令参数将 setnx 和 expire 合并成一条指令</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置name=zcm，失效时长100s，不存在时设置</span></span><br><span class="line">set name zcm ex 100 nx</span><br></pre></td></tr></table></figure>
</blockquote>
</li>
<li><p>RedLock 简介</p>
<blockquote>
<p><strong>Redlock：全名叫做 Redis Distributed Lock，即使用 redis 实现的分布式锁</strong></p>
<p>使用场景：多个服务间保证同一时刻同一用户只能有一个请求（防止关键业务出现并发攻击）</p>
<p><strong>最低保证分布式锁的有效性及安全性的要求如下</strong></p>
<ul>
<li>互斥：任何时刻只能有一个 client 获取锁</li>
<li>释放死锁：即使锁定资源的服务崩溃或者分区，仍然能释放锁</li>
<li>容错性：只要多数 redis 节点（一半以上）在使用，client 就可以获取和释放锁</li>
</ul>
<p><strong>多节点 redis 实现的分布式锁算法（RedLock）可以有效防止单点故障</strong></p>
<p>在 client 重试获取锁的间隔（第一次失败到第二次重试时间间隔）大于第一次获取锁消耗的时间</p>
<p><strong>重试获取锁要有一定次数限制</strong></p>
</blockquote>
</li>
<li><p>加锁过程</p>
<ul>
<li><p>获取当前时间戳</p>
</li>
<li><p>client 尝试按照顺序使用相同的 key-value 获取所有 redis 服务的锁，在获取锁的过程中的获取时间比锁过期时间短很多，这是为了不要过长时间等待已经关闭的 redis 服务。并且试着获取下一个 redis 实例</p>
<blockquote>
<p>比如：TTL 为10s，设置获取锁最多用 100ms，所以如果 100ms 内无法获取锁，就放弃获取这个锁（视为获取锁失败），从而尝试获取下个锁</p>
</blockquote>
</li>
<li><p>client 通过获取所有能获取的锁后的时间减去第一步的时间，这个时间差要小于 TTL 时间并且至少有 3 个 redis 实例成功获取锁（类似选举，要求过半成功），才算真正的获取锁成功</p>
</li>
<li><p>如果成功获取锁，则锁的真正有效时间是 TTL 减去第三步的时间差的时间</p>
<blockquote>
<p>比如：TTL 是 10s，获取所有锁用了 300ms，则真正锁有效时间为 9700ms</p>
</blockquote>
</li>
<li><p>如果客户端由于某些原因获取锁失败，便会开始解锁所有 redis 实例</p>
<blockquote>
<p>因为可能已经获取了小于3个锁，必须释放，否则影响其他 client 获取锁</p>
</blockquote>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/redlock%E5%8A%A0%E9%94%81.png" alt="redlock加锁"></p>
<blockquote>
<p><strong>每个 client 的 UUID 不同，保证互斥和可重入</strong></p>
</blockquote>
</li>
<li><p>释放锁过程</p>
<ul>
<li><p>主动释放（任务完成）</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E4%B8%BB%E5%8A%A8%E9%87%8A%E6%94%BE.png" alt="主动释放"></p>
</li>
<li><p>被动释放（TTL 到期）</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E8%A2%AB%E5%8A%A8%E9%87%8A%E6%94%BE.png" alt="被动释放"></p>
</li>
</ul>
</li>
<li><p>RedLock 存在问题</p>
<ul>
<li><p>宕机重启后 2 个客户端拿到同一把锁</p>
<blockquote>
<p>假设 5 个节点是 A, B, C, D, E，客户端 1 在 A, B, C 上面拿到锁，D, E 没有拿到锁，客户端 1 拿锁成功</p>
<p>此时 C 挂了重启，C 上面锁的数据丢失。客户端2去取锁，从 C, D, E 3 个节点拿到锁，A, B 没有拿到（还被客户端1持有），客户端2也超过多数派，也会拿到锁</p>
</blockquote>
</li>
<li><p>脑裂问题</p>
<blockquote>
<p>比如有节点 1、2、3、4、5，客户端 A、B、C 同时竞争锁，A 获得1、2；B获得3、4；C获得5。最后 A、B、C 都没有成功获得锁（没有获得半数以上的锁）</p>
<p>官方的建议是尽量同时并发的向所有节点发送获取锁命令。客户端取得大部分 Redis 实例锁所花费的时间越短，脑裂出现的概率就会越低</p>
</blockquote>
</li>
<li><p>效率低</p>
<blockquote>
<p>主节点越多，获取锁的时间越长</p>
</blockquote>
</li>
</ul>
</li>
<li><p>Redis 分布式锁如何续期？看门狗（Watchdog）知道吗？</p>
<blockquote>
<p>Redisson 锁有两种模式</p>
<ul>
<li>固定有效期的锁：超过有效期 TTL 后，自动释放锁</li>
<li>没有有效期的锁：默认30秒，然后采用 Watchdog 进行续期，直到业务逻辑执行完毕</li>
</ul>
</blockquote>
<blockquote>
<p>watchdog 续期</p>
<p><strong>每 1&#x2F;3 的锁时间检查1次，如果线程还持有锁，则刷新过期时间</strong></p>
<p>实例：</p>
<p>加锁的时间是 30 秒</p>
<p>如果加锁的业务没有执行完，那么到 30 - 10 &#x3D; 20s 的时候就会进行一次续期，把锁重置成 30 秒</p>
<p>如果业务的机器宕机则无法续期，30s 后自动解锁</p>
</blockquote>
</li>
</ol>
<h3 id="35-Redis-支持事务吗？能保证-ACID-吗？"><a href="#35-Redis-支持事务吗？能保证-ACID-吗？" class="headerlink" title="35.Redis 支持事务吗？能保证 ACID 吗？"></a>35.Redis 支持事务吗？能保证 ACID 吗？</h3><blockquote>
<p><strong>Redis 部分支持事务，不能保证 ACID</strong></p>
<p><strong>Redis 事务是一个单独的隔离操作：事务中的所有命令都会序列化、按顺序地执行</strong></p>
<p>事务在执行的过程中，不会被其他客户端发送来的命令请求所打断</p>
<p><strong>Redis 事务的主要作用就是串联多个命令防止别的命令插队</strong></p>
</blockquote>
<ul>
<li><p>常用命令</p>
<ul>
<li><code>MULTI</code>：开启一个事务，MULTI 执行之后，客户端可以继续向服务器发送任意多条命令，这些命令不会立即被执行，而是被放到一个队列中</li>
<li><code>EXEC</code>：执行队列中所有的命令</li>
<li><code>DISCARD</code>：清空事务队列，并放弃执行事务</li>
<li><code>UNWATCH</code>：取消 <code>WATCH</code> 命令对所有 key 的监视</li>
<li><code>WATCH key1 key2 ...</code>	：监视一个(或多个) key ，如果在事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断</li>
</ul>
</li>
<li><p>事务处理举例</p>
<blockquote>
<p>从输入 Multi 命令开始，输入的命令都会依次进入命令队列中，但不会执行，直到输入 Exec后，Redis 会将之前的命令队列中的命令依次执行</p>
<p>组队的过程中可以通过 discard 来放弃组队</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135314482.png" alt="image-20221013135314482"></p>
</blockquote>
<blockquote>
<p>案例1：组队成功，提交成功</p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135344421.png" alt="image-20221013135344421"></p>
<p>案例2：组队阶段报错，提交失败</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135437111.png" alt="image-20221013135437111"></p>
<p>案例3：组队成功，提交阶段有成功有失败</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135420516.png" alt="image-20221013135420516"></p>
</blockquote>
</li>
<li><p>事务的错误处理</p>
<ul>
<li><p>组队过程中某个命令出现了报告错误，执行时整个的所有队列都会被取消</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135537287.png" alt="image-20221013135537287"></p>
</li>
<li><p>如果执行阶段某个命令报出了错误，则只有报错的命令不会被执行，而其他的命令都会执行，不会回滚</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/image-20221013135547886.png" alt="image-20221013135547886"></p>
</li>
</ul>
</li>
<li><p>事务冲突问题</p>
<blockquote>
<p>缓存的数据，谁都可以拿，可以改，所以必须打标记来监控行为</p>
<p>这里涉及到锁的问题：悲观锁 &#x2F; 乐观锁 &#x2F; CAS（Check And Set）</p>
</blockquote>
<ul>
<li><p><strong>悲观锁（Pessimistic Lock）</strong></p>
<blockquote>
<p>每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会 block 直到它拿到锁</p>
<p>传统的关系型数据库里边就用到了很多这种锁机制，比如行锁，表锁等，读锁，写锁等，都是在做操作之前先上锁</p>
</blockquote>
</li>
<li><p><strong>乐观锁（Optimistic Lock）</strong></p>
<blockquote>
<p>每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下，在此期间别人有没有去更新这个数据，可以使用版本号等机制</p>
<p>乐观锁适用于多读的应用类型，这样可以提高吞吐量</p>
<p>乐观锁策略（常用）：提交版本必须大于记录当前版本才能执行更新</p>
<p>这样既不影响并发性，有可以满足需求</p>
</blockquote>
</li>
<li><p>举例1：watch 无冲突</p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/watch.png" alt="watch"></p>
</li>
<li><p>举例2：watch 有冲突</p>
<blockquote>
<p>在 WATCH 监控后，有人修改了balance，会导致事务会被打断，必须更新最新值，才能成功执行事务，类似于乐观锁的版本号机制</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/watch%E5%8A%A0%E5%A1%9E.png" alt="watch加塞"></p>
</li>
<li><p>unwatch</p>
<blockquote>
<p>取消 WATCH 命令对所有 key 的监视</p>
<p>如果在执行 WATCH 命令之后，EXEC 命令或 DISCARD 命令先被执行了的话，那么就不需要再执行 UNWATCH 了</p>
</blockquote>
</li>
</ul>
</li>
<li><p>Redis 事务的特性</p>
<blockquote>
<p><strong>Redis 的事务机制可以保证一致性和隔离性</strong></p>
<p><strong>Redis 的持久化 RDB、AOF 都可能造成数据丢失，无法保证持久性</strong></p>
<p>不过因为 Redis 本身是内存数据库，持久性并不是一个必须的属性，更加关注的还是<code>原子性、一致性、隔离性</code>这三个属性</p>
<p>原子性的情况比较复杂，<strong>当事务中使用的命令语法有误时，原子性得不到保证</strong>，在其它情况下，事务都可以原子性执行</p>
</blockquote>
<ul>
<li><p><strong>单独的隔离操作</strong></p>
<blockquote>
<p>事务中的所有命令都会序列化、按顺序地执行</p>
<p>事务在执行的过程中，不会被其他客户端发送来的命令请求所打断</p>
</blockquote>
</li>
<li><p><strong>没有隔离级别的概念</strong></p>
<blockquote>
<p>队列中的命令没有提交之前都不会实际被执行，因为事务提交前任何指令都不会被实际执行</p>
</blockquote>
</li>
<li><p><strong>不保证原子性</strong></p>
<blockquote>
<p>事务中如果有一条命令执行失败，其后的命令仍然会被执行，没有回滚</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="36-Redis-Pipeline-详解"><a href="#36-Redis-Pipeline-详解" class="headerlink" title="36.Redis Pipeline 详解"></a>36.Redis Pipeline 详解</h3><ol>
<li><p>pipeline 定义</p>
<blockquote>
<p>pipeline 就是管道，管道本身是能够承载流式数据的一个长链路，可以进行事件的缓冲</p>
</blockquote>
</li>
<li><p>pipeline 请求模型</p>
<blockquote>
<p>普通请求模型</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/%E6%99%AE%E9%80%9A%E8%AF%B7%E6%B1%82.png" alt="普通请求"></p>
<blockquote>
<p>pipeline 请求模型</p>
<p>管道（pipeline）可以一次性发送多条命令给服务端，服务端依次处理完完毕后，通过一条响应一次性将结果返回</p>
<p>pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间，而且 Pipeline 实现的原理是队列，而队列的原理是时先进先出，这样就保证数据的顺序性</p>
<p>Pipeline 的默认的同步的个数为 53 个，也就是说 arges 中累加到 53 条数据时会把数据提交</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/pipeline%E6%A8%A1%E5%9E%8B.png" alt="pipeline模型"></p>
</li>
<li><p>pipeline 与其他对比</p>
<ul>
<li><p><strong>Pipeline 与原生批量命令对比</strong></p>
<blockquote>
<p>原生批量命令是原子性（例如：mset、mget），pipeline 是非原子性</p>
<p>原生批量命令一次只能执行一种命令，pipeline 支持批量执行不同命令</p>
<p>原生批命令是服务端实现，而 pipeline 需要服务端与客户端共同完成</p>
</blockquote>
</li>
<li><p><strong>Pipeline 与事务对比</strong></p>
<blockquote>
<p>事务具有隔离性，在执行过程中不会穿插执行其他客户端发送的命令</p>
</blockquote>
</li>
<li><p><strong>pipeline 与 lua 脚本对比</strong></p>
<blockquote>
<p>脚本可以实现类似 pipeline 一次 I&#x2F;O 处理多个命令的场景，同时可以解决命令之间的依赖问题</p>
<p>pipeline 由于是发送批量请求，但是一次响应结果，所以在拿到响应结果之前无法判断操作是否成功，即发送的批量请求之间不支持相互依赖</p>
<p>lua脚本同样是客户端一次请求，服务端执行多个命令，由于脚本可以支持判断，所以可以支持命令之间的相互依赖和逻辑处理</p>
</blockquote>
</li>
</ul>
</li>
<li><p>使用 pipeline 注意事项</p>
<ul>
<li><p>使用 pipeline 组装的命令个数不能太多，不然数据量过大，会造成以下两个问题</p>
<blockquote>
<p>客户端执行 exec 发送缓冲命令到服务端后，将阻塞进程，直到服务端处理完 pipeline 发送的所有命令并返回结果后才能继续响应其他请求，当数据量过大时，客户端阻塞的时间可能过久，同时服务端此时也被迫回复一个队列答复，占用很多内存</p>
<p>客户端发送命令之前，命令都将存储在客户端缓冲区</p>
<p>pipeline 事实上所能容忍的操作个数，和 socket-output 缓冲区大小 &#x2F; 返回结果的数据尺寸都有很大的关系（默认每个套接字缓冲区的大小为 8K）</p>
<p>同时也意味着每个 redis-server 同时所能支撑的 pipeline 链接的个数也是有限的，这将受限于 server 的物理内存或网络接口的缓冲能力</p>
</blockquote>
</li>
<li><p>关于 pipeline 的时序性问题</p>
<blockquote>
<p>pipeline 缓冲的指令发送到服务器端时，本身是按照指令缓冲的顺序执行</p>
<p>因为缓冲指令是使用队列的方式实现的，但是缓冲指令中间可能会穿插其他客户端发送来的命令</p>
</blockquote>
</li>
<li><p>关于 pipeline 的原子性问题</p>
<blockquote>
<p>pipeline 缓冲的指令只是会依次执行，但是不能保证原子性</p>
<p>如果执行中间某一个指令发生异常，将会继续执行后续的指令</p>
</blockquote>
</li>
<li><p>cluster 并不支持 pipeline 操作</p>
<blockquote>
<p><strong>pipeline 每次只能作用在一个 Redis 节点上</strong></p>
</blockquote>
</li>
</ul>
</li>
<li><p>pipeline 适用场景</p>
<blockquote>
<p><strong>不合适一次操作强依赖结果的值</strong></p>
<p>有些系统可能对可靠性要求很高，每次操作都需要立马知道这次操作是否成功，是否数据已经写进redis 了，那这种场景就不适合</p>
<p><strong>有的系统，可能是批量的将数据写入 redis，允许一定比例的写入失败，那么这种场景就可以使用了</strong></p>
<p>比如 10000 条一下进入 redis，可能失败了 2 条无所谓，后期有补偿机制就行了，比如短信群发这种场景，如果一下群发 10000 条，按照第一种模式去实现，那这个请求过来，要很久才能给客户端响应，这个延迟就太长了，如果客户端请求设置了超时时间 5 秒，那肯定就抛出异常了，而且本身群发短信要求实时性也没那么高，这时候用 pipeline 最好了</p>
</blockquote>
</li>
</ol>
<h3 id="37-Redis-适用场景"><a href="#37-Redis-适用场景" class="headerlink" title="37.Redis 适用场景"></a>37.Redis 适用场景</h3><ol>
<li><p><strong>会话缓存（ Session Cache）</strong> </p>
<blockquote>
<p>最常用的一种使用 Redis 的情景是会话缓存（ session cache）</p>
<p>用 Redis 缓存会话比其他存储（ 如Memcached）的优势在于：Redis 提供持久化</p>
<p>当维护一个不是严格要求一致性的缓存时， 如果用户的购物车信息全部丢失， 大部分人都会不高兴的， 幸运的是， 随着 Redis 这些年的改进， 很容易找到怎么恰当的使用 Redis 来缓存会话的文档</p>
</blockquote>
</li>
<li><p><strong>全页缓存（ FPC）</strong> </p>
<blockquote>
<p>除基本的会话 token 之外， Redis 还提供很简便的 FPC 平台</p>
<p>回到一致性问题， 即使重启了 Redis 实例， 因为有磁盘的持久化， 用户也不会看到页面加载速度的下降，这是一个极大改进，类似 PHP 本地 FPC</p>
</blockquote>
</li>
<li><p><strong>队列</strong></p>
<blockquote>
<p>Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作， 这使得 Redis 能作为一个很好的<strong>消息队列</strong>平台来使用</p>
<p>Redis 作为队列使用的操作，就类似于本地程序语言（ 如 Python）对 list 的 push&#x2F;pop 操作</p>
</blockquote>
</li>
<li><p><strong>排行榜 &#x2F; 计数器</strong></p>
<blockquote>
<p>Redis 在内存中对数字进行递增或递减的操作实现的非常好</p>
<p>集合（ Set） 和有序集合（ Sorted Set）也使得在执行这些操作的时候变的非常简单，Redis 只是正好提供了这两种数据结构</p>
</blockquote>
</li>
<li><p><strong>发布 &#x2F; 订阅</strong></p>
<blockquote>
<p>发布 &#x2F; 订阅的使用场景确实非常多</p>
<p>在社交网络连接中使用， 还可作为基于发布&#x2F;订阅的脚本触发器， 甚至用 Redis 的发布&#x2F;订阅功能来建立聊天系统</p>
</blockquote>
</li>
</ol>
<h3 id="38-Redis-异步队列"><a href="#38-Redis-异步队列" class="headerlink" title="38.Redis 异步队列"></a>38.Redis 异步队列</h3><ol>
<li><p>使用 redis 作为异步队列</p>
<blockquote>
<p>一般使用 list 结构作为队列</p>
<p>rpush 生产消息、lpop 消费消息</p>
<p>当 lpop 没有消息时，sleep 一段时间后重试</p>
</blockquote>
</li>
<li><p>追问</p>
<ul>
<li><p>可不可以不用 sleep</p>
<blockquote>
<p>如不用 sleep，可以用 blpop，没有消息时直接阻塞，直到有消息</p>
</blockquote>
</li>
<li><p>能否实现一次生产多次消费</p>
<blockquote>
<p>使用 pub&#x2F;sub 主题订阅模式可以实现 1:N 的消息队列（即一次生产多次消费）</p>
</blockquote>
</li>
<li><p>pub&#x2F;sub 的缺点</p>
<blockquote>
<p>消费者下线时，生产的消息会丢失</p>
<p>需要使用专业的消息队列如 RabbitMQ 等</p>
</blockquote>
</li>
<li><p>redis 如何实现延时队列</p>
<blockquote>
<p>使用 zset，消息内容为 key、时间戳为 score</p>
<p>消费者用 <code>zrangebyscore</code> 指令可以获取 N 秒之前的数据作轮询处理</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h3 id="39-其他"><a href="#39-其他" class="headerlink" title="39.其他"></a>39.其他</h3><ol>
<li><a target="_blank" rel="noopener" href="https://www.cnblogs.com/linguanh/p/10460421.html">HyperLogLog 算法原理</a></li>
<li><a target="_blank" rel="noopener" href="https://www.cnblogs.com/mumage/p/12832766.html">Redis 多线程详解</a></li>
</ol>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="https://gitee.com/zcmmmm/zcmmmm">zcm</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="https://gitee.com/zcmmmm/zcmmmm/2022/10/14/Redis%E9%9D%A2%E7%BB%8F/">https://gitee.com/zcmmmm/zcmmmm/2022/10/14/Redis%E9%9D%A2%E7%BB%8F/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="https://gitee.com/zcmmmm/zcmmmm" target="_blank">无人深空</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/Redis/">Redis</a><a class="post-meta__tags" href="/tags/DataBase/">DataBase</a></div><div class="post_share"><div class="social-share" data-image="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis.png" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/2022/10/15/Nginx%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"><img class="prev-cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Nginx/Nginx.png" onerror="onerror=null;src='/img/404.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">Nginx基础知识</div></div></a></div><div class="next-post pull-right"><a href="/2022/10/12/Redis6%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"><img class="next-cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis.png" onerror="onerror=null;src='/img/404.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">Redis6基础知识</div></div></a></div></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><div><a href="/2022/10/12/Redis6%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/" title="Redis6基础知识"><img class="cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/Redis/Redis.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-10-12</div><div class="title">Redis6基础知识</div></div></a></div></div></div></div><div class="aside-content" id="aside-content"><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content is-expand"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#Redis-%E9%9D%A2%E7%BB%8F"><span class="toc-text">Redis 面经</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#1-Redis-%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%8E%9F%E7%90%86"><span class="toc-text">1.Redis 单线程原理</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2-Redis-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%EF%BC%885-3%EF%BC%89"><span class="toc-text">2.Redis 数据类型（5 + 3）</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#3-%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E7%94%A8-Redis"><span class="toc-text">3.什么情况下用 Redis</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-Redis-%E4%B8%8E-memcache-%E7%9A%84%E5%8C%BA%E5%88%AB"><span class="toc-text">4.Redis 与 memcache 的区别</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#5-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F"><span class="toc-text">5.简述缓存穿透</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#6-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95"><span class="toc-text">6.简述缓存穿透的解决方法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#7-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF"><span class="toc-text">7.简述缓存击穿</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#8-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95"><span class="toc-text">8.简述缓存击穿的解决方法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#9-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9"><span class="toc-text">9.简述缓存雪崩</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#10-%E7%AE%80%E8%BF%B0%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95"><span class="toc-text">10.简述缓存雪崩的解决方法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#11-Redis-%E6%9C%89%E5%93%AA%E4%BA%9B%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2%E6%96%B9%E5%BC%8F"><span class="toc-text">11.Redis 有哪些集群部署方式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#12-%E7%AE%80%E8%BF%B0%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E6%A8%A1%E5%BC%8F"><span class="toc-text">12.简述主从复制模式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#13-%E7%AE%80%E8%BF%B0%E5%93%A8%E5%85%B5%E6%A8%A1%E5%BC%8F"><span class="toc-text">13.简述哨兵模式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#14-cluster-%E9%9B%86%E7%BE%A4"><span class="toc-text">14.cluster 集群</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#15-%E7%AE%80%E8%BF%B0-Redis-%E7%9A%84-RDB"><span class="toc-text">15.简述 Redis 的 RDB</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#16-%E7%AE%80%E8%BF%B0-Redis-%E7%9A%84-save-%E5%91%BD%E4%BB%A4"><span class="toc-text">16.简述 Redis 的 save 命令</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#17-%E7%AE%80%E8%BF%B0-Redis-%E7%9A%84-bgsave-%E5%91%BD%E4%BB%A4"><span class="toc-text">17.简述 Redis 的 bgsave 命令</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#18-%E7%AE%80%E8%BF%B0-Redis-%E8%87%AA%E5%8A%A8%E8%A7%A6%E5%8F%91-RDB-%E6%9C%BA%E5%88%B6"><span class="toc-text">18.简述 Redis 自动触发 RDB 机制</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#19-%E7%AE%80%E8%BF%B0-Redis-%E7%9A%84-AOF"><span class="toc-text">19.简述 Redis 的 AOF</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#20-%E7%AE%80%E8%BF%B0-AOF-%E7%9A%84%E6%8C%81%E4%B9%85%E5%8C%96%E7%AD%96%E7%95%A5%EF%BC%88%E5%86%99%E5%85%A5%E7%AD%96%E7%95%A5%EF%BC%89"><span class="toc-text">20.简述 AOF 的持久化策略（写入策略）</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#21-%E7%AE%80%E8%BF%B0-AOF-%E7%9A%84%E9%87%8D%E5%86%99"><span class="toc-text">21.简述 AOF 的重写</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#22-RDB-%E4%B8%8E-AOF-%E4%BC%98%E7%BC%BA%E7%82%B9%E6%AF%94%E8%BE%83"><span class="toc-text">22.RDB 与 AOF 优缺点比较</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#23-MySQL-%E4%B8%8E-Redis-%E5%8C%BA%E5%88%AB"><span class="toc-text">23.MySQL 与 Redis 区别</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#24-%E7%AE%80%E8%BF%B0-Redis-%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5"><span class="toc-text">24.简述 Redis 过期策略</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#25-%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%8E%E7%BC%93%E5%AD%98%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98"><span class="toc-text">25.数据库与缓存不一致问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#26-%E7%BC%93%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5"><span class="toc-text">26.缓存淘汰策略</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#27-%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8"><span class="toc-text">27.布隆过滤器</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#28-%E8%AE%BE%E7%BD%AE-redis-%E5%86%85%E5%AD%98"><span class="toc-text">28.设置 redis 内存</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#29-Redis-%E5%A4%A7-key-%E9%97%AE%E9%A2%98"><span class="toc-text">29.Redis 大 key 问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#30-%E8%B7%B3%E8%A1%A8%E8%AF%A6%E8%A7%A3"><span class="toc-text">30.跳表详解</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#31-MySQL-%E9%87%8C%E6%9C%89-2000w-%E6%95%B0%E6%8D%AE%EF%BC%8CRedis-%E4%B8%AD%E5%8F%AA%E5%AD%98-20w-%E7%9A%84%E6%95%B0%E6%8D%AE%EF%BC%8C%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Redis%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E9%83%BD%E6%98%AF%E7%83%AD%E7%82%B9%E6%95%B0%E6%8D%AE"><span class="toc-text">31.MySQL 里有 2000w 数据，Redis 中只存 20w 的数据，如何保证Redis中的数据都是热点数据?</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#32-Redis%E5%AD%98%E5%82%A8%E5%AF%B9%E8%B1%A1%E4%BF%A1%E6%81%AF%E6%98%AF%E7%94%A8Hash%E8%BF%98%E6%98%AFString%E5%A5%BD%EF%BC%9F"><span class="toc-text">32.Redis存储对象信息是用Hash还是String好？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#33-Redis-%E7%9A%84-IO-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%EF%BC%8C%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8D%95%E7%BA%BF%E7%A8%8B%E8%BF%98%E5%8F%AF%E4%BB%A5%E6%8A%97%E9%82%A3%E4%B9%88%E9%AB%98%E7%9A%84-QPS-Query-Per-Second"><span class="toc-text">33.Redis 的 IO 多路复用如何理解，为什么单线程还可以抗那么高的 QPS(Query Per Second)</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#34-Redis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81-RedLock"><span class="toc-text">34.Redis 分布式锁 RedLock</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#35-Redis-%E6%94%AF%E6%8C%81%E4%BA%8B%E5%8A%A1%E5%90%97%EF%BC%9F%E8%83%BD%E4%BF%9D%E8%AF%81-ACID-%E5%90%97%EF%BC%9F"><span class="toc-text">35.Redis 支持事务吗？能保证 ACID 吗？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#36-Redis-Pipeline-%E8%AF%A6%E8%A7%A3"><span class="toc-text">36.Redis Pipeline 详解</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#37-Redis-%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF"><span class="toc-text">37.Redis 适用场景</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#38-Redis-%E5%BC%82%E6%AD%A5%E9%98%9F%E5%88%97"><span class="toc-text">38.Redis 异步队列</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#39-%E5%85%B6%E4%BB%96"><span class="toc-text">39.其他</span></a></li></ol></li></ol></div></div></div></div></main><footer id="footer"><div id="footer-wrap"><div class="copyright">&copy;2022 - 2023 By zcm</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><button id="go-up" type="button" title="回到顶部"><i class="fas fa-arrow-up"></i></button></div></div><div id="local-search"><div class="search-dialog"><nav class="search-nav"><span class="search-dialog-title">搜索</span><span id="loading-status"></span><button class="search-close-button"><i class="fas fa-times"></i></button></nav><div class="is-center" id="loading-database"><i class="fas fa-spinner fa-pulse"></i><span>  数据库加载中</span></div><div class="search-wrap"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div><hr/><div id="local-search-results"></div></div></div><div id="search-mask"></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.js"></script><script src="/js/search/local-search.js"></script><div class="js-pjax"></div><script src="/js/categories.js?v1"></script><script src="/js/navigation.js?v1"></script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>